https://bicon.gumyo.net/

 

BIcon

Generate Badge icon easily

bicon.gumyo.net

 

 

설날 휴일 첫째날인 오늘.. 아침에 심각한 고민에 빠졌다

 

"뭐하지.."

 

게임하기에는 다 재미가 없고 뭔가 만들자니 애매한데.. 그렇게 유튜브랑 허공을 보다가 2시간이 흐르고 오전 10시가 되었다

 

github의 READMD.md페이지를 보면서 

 

"아 ! 이거 ! 뱃지 생성기 만들려고 했는데 지금 하면 되겠네!"

 

 

머리에 스치는 스택은 대충 

 JSDOM

htmltoimage

이런 것들만 생각이 나는데..

 

몇 주 전인가 nextjs 도큐먼트를 재미 삼아 보면서 발견한 기능이 있었는데 그건 바로.. 

https://nextjs.org/docs/app/api-reference/functions/image-response

 

Functions: ImageResponse | Next.js

API Reference for the ImageResponse constructor.

nextjs.org

 

사실 이번 아이디어도 저거보다 나온 거다 

 

그래서 "오케이 nextjs 단독으로 함 작성해 보자"

 

 

그렇게 메인 스택이 nextjs, UI 짜면 하루정도 더 걸리니까

그냥 이미 다 짜져 있는 shadcn을 쓰기로 한다 

 

https://ui.shadcn.com/

 

shadcn/ui

Beautifully designed components that you can copy and paste into your apps. Accessible. Customizable. Open Source.

ui.shadcn.com

흑흑 사랑해요 shadcn.. 너무 이뻐..

 

 

 

일단 nextjs와 shadcn을 초기화한다

 

nextjs는 이미 다 인터넷에 올라와있고..

shadcn도 인스톨 한 뒤에 

npx shadcn-ui@latest add

 

치고 a 누르면 전체 선택.. 그리고 엔터 치면 바로 전체 사용이 가능하다

 

 

일단 페이지를 대충 슥슥 그리고 

 

오늘의 메인 기능은 이미지 뽑는 api를 nextjs에서 작성해 보자 

 

 

app폴더 아래에 api폴더를 하나 작성해 주고

그 아래에 subpath가 될 폴더를 또 작성해 준다 (나는 icon으로 작성)

 

그리고 tsx파일을 route.tsx로 작성해준다 

 

import { PROPERTIES, PropertyMap } from '@/lib/constant'
import { headers } from 'next/headers'
import { ImageResponse } from 'next/og'
import { NextRequest } from 'next/server'
export const runtime = 'edge'
export const GET = async (request: NextRequest) => {
    const headersList = headers()
    const domain = headersList.get('x-forwarded-host')
    const origin = headersList.get('x-forwarded-proto')
    const currentURL = `${origin}://${domain}`
    const { searchParams } = new URL(request.url)
    const { width, height, icon, cIcon, iconBgColor, bgColor, textColor, text, borderRadius } = Object.keys(PROPERTIES).reduce(
        (prev, next) => ({ ...prev, [next]: searchParams.get(next) }),
        {},
    ) as PropertyMap

    const props = {
        width: Number(width) || 100,
        height: Number(height) || 25,
        bgColor: bgColor ? bgColor : 'transparent',
        textColor: textColor ? textColor : '#000',
        iconBgColor: iconBgColor ? iconBgColor : 'transparent',
        text: text || '',
        icon: cIcon || `${currentURL}/${icon}.svg` || `${currentURL}/logo.svg`,
        borderRadius: (Number(borderRadius) || 0) + 'px',
    }

    return new ImageResponse(
        (
            <div
                style={{
                    width: props.width + 'px',
                    height: props.height + 'px',
                    backgroundColor: props.bgColor,
                    display: 'flex',
                    justifyContent: 'flex-start',
                    alignItems: 'baseline',
                    borderRadius: props.borderRadius,
                    overflow: 'hidden',
                }}
            >
                <div
                    style={{
                        backgroundColor: props.iconBgColor,
                        width: props.height + 'px',
                        height: props.height + 'px',
                        display: 'flex',
                        justifyContent: 'center',
                        alignItems: 'center',
                    }}
                >
                    {/* eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element */}
                    <img
                        src={props.icon}
                        style={{
                            objectFit: 'contain',
                            width: props.height - props.height / 5,
                            height: props.height - props.height / 5,
                            marginLeft: props.height - props.height / 1.5,
                        }}
                    />
                </div>
                <span
                    style={{
                        color: props.textColor,
                        textAlign: 'center',
                        flex: 1,
                        justifyContent: 'center',
                        fontSize: props.height / 1.5 + 'px',
                    }}
                >
                    {props.text}
                </span>
            </div>
        ),
        {
            width: props.width,
            height: props.height,
            fonts: [
                {
                    name: 'M PLUS Rounded 1c',
                    data: await fetch(new URL(currentURL + '/mplus.ttf', currentURL)).then((res) => res.arrayBuffer()),
                    weight: 100,
                    style: 'normal',
                },
            ],
        },
    )
}

 

전문을 긁어오긴 했는데 하나씩 떼서보자 

 

먼저 

    const headersList = headers()
    const domain = headersList.get('x-forwarded-host')
    const origin = headersList.get('x-forwarded-proto')
    const currentURL = `${origin}://${domain}`
    const { searchParams } = new URL(request.url)
    const { width, height, icon, cIcon, iconBgColor, bgColor, textColor, text, borderRadius } = Object.keys(PROPERTIES).reduce(
        (prev, next) => ({ ...prev, [next]: searchParams.get(next) }),
        {},
    ) as PropertyMap

    const props = {
        width: Number(width) || 100,
        height: Number(height) || 25,
        bgColor: bgColor ? bgColor : 'transparent',
        textColor: textColor ? textColor : '#000',
        iconBgColor: iconBgColor ? iconBgColor : 'transparent',
        text: text || '',
        icon: cIcon || `${currentURL}/${icon}.svg` || `${currentURL}/logo.svg`,
        borderRadius: (Number(borderRadius) || 0) + 'px',
    }

 

각종 변수들을 싹 정리해 주자 

 

next/header에서 제공하는 기능으로 header에서 domain과 origin을 뽑아서

굳이 env로 뺄 필요 없이 도메인을 자동으로 작성해 주자

 

두 번째로는 request에서 긁어온  url에서 searchParam을 싸악 긁어오자

reduce한방이면 object로 말아서 export 할 수가 있다 

 

미리 들어올 object에 대한 type도 작성해 두었다 

코드가 너무 길어서 사진으로 대체한다

그리고 이제 render에서 쓰일 변수들을 싸악 다시 props로 재구성해준다

null처리랑 기본 값 등등.. 잘 선언해 주자 

 

return new ImageResponse(
        (
            <div
                style={{
                    width: props.width + 'px',
                    height: props.height + 'px',
                    backgroundColor: props.bgColor,
                    display: 'flex',
                    justifyContent: 'flex-start',
                    alignItems: 'baseline',
                    borderRadius: props.borderRadius,
                    overflow: 'hidden',
                }}
            >
                <div
                    style={{
                        backgroundColor: props.iconBgColor,
                        width: props.height + 'px',
                        height: props.height + 'px',
                        display: 'flex',
                        justifyContent: 'center',
                        alignItems: 'center',
                    }}
                >
                    {/* eslint-disable-next-line jsx-a11y/alt-text, @next/next/no-img-element */}
                    <img
                        src={props.icon}
                        style={{
                            objectFit: 'contain',
                            width: props.height - props.height / 5,
                            height: props.height - props.height / 5,
                            marginLeft: props.height - props.height / 1.5,
                        }}
                    />
                </div>
                <span
                    style={{
                        color: props.textColor,
                        textAlign: 'center',
                        flex: 1,
                        justifyContent: 'center',
                        fontSize: props.height / 1.5 + 'px',
                    }}
                >
                    {props.text}
                </span>
            </div>
        ),
        {
            width: props.width,
            height: props.height,
            fonts: [
                {
                    name: 'M PLUS Rounded 1c',
                    data: await fetch(new URL(currentURL + '/mplus.ttf', currentURL)).then((res) => res.arrayBuffer()),
                    weight: 100,
                    style: 'normal',
                },
            ],
        },
    )

 

이번 글의 하이라이트인 ImageResponse.. 라기엔 너무 초라하긴 하다 

 

그냥 HTML로 써서 사진으로 나타낼 정보를 그려주자 

 

각 값들은 props, 위에서 구조분해 할당으로 선언한 변수들로 값을 채워주면 api/icon에 대한 라우팅은 끝이다 

 

실제로 postman으로 값 찍어보면 이렇게 나온다 

아주 잘 나온다

 

이제 이 라우터를 가지고 nextjs에서 페이지를 작성하면 된다 

 

이다음부터는 shadcn과 기초적인 react이기에 따로 설명은 필요 없고.. github페이지 하나 남겨둔다

https://github.com/B-HS/BIcon/blob/main/app/page.tsx

 

 

 

아침부터 10시간? 정도 삽질하고 작성하고 중간에 밥도 먹고 술도 먹고.. 

 

생각했던 것이 하루 만에 끝내서 재밌게 한 프로젝트인듯하다

 

shadcn이 막대한 역할을 했고..

하마터면 JSDOM과 htmlToImage로

또 다른 삽질 할 것을 막아준 nextjs에게 무한한 감사..

 

모든 소스코드는 아래에 있으니 관심이 있으면 들러주시고 별 하나 박아주시면 감사합니다..

https://github.com/B-HS/BIcon

 

GitHub - B-HS/BIcon: Generate badge icon using nextjs

Generate badge icon using nextjs. Contribute to B-HS/BIcon development by creating an account on GitHub.

github.com

 

 

복사했습니다!