๊ตญ๊ฐ ํธ๋ ๋ ๋ถ์๋ถํฐ ์ฝ์ค ์ถ์ฒ๊น์ง, ๋ฐฑํ์ ์ธ๊ตญ์ธ ๋ฐฉ๋ฌธ๊ฐ์ ์ํ AI ์ผํ ๋ฉ์ดํธ
Next.js 16 App Router ยท React 19 ยท Three.js ยท Tailwind CSS ยท Zustand ยท next-intl ยท PWA
HI-FO! 3ํ
| ํ์ฅ | ๋ถํ์ฅ | ํ์ | ํ์ |
|---|---|---|---|
| ๋๊ฑด์ฐ | ๊ณตํฌ์ง | ์์์ฐฌ | ์์ ๋ฏผ |
| @woodgeon | @heejinkong | @Ui-chan | @Dev-Anniee |
ํ๋ก์ ํธ ์๊ฐ ยท ์ฃผ์ ํ๋ฉด ยท ํ๋ก ํธ์๋๊ฐ ํธ๋ ๋ฌธ์ ยท ๊ฐ๋ฐ ๊ฐ์ด๋
์ธ๊ตญ์ธ์ด ํ๊ตญ์ ๊ด์ฌ์ ๊ฐ๊ฒ ๋ ๊ณ๊ธฐ 1์๋ ํ๋ฅ ์ฝํ ์ธ 38.3%(K-POP ยท DRAMA ยท K-BEAUTY ยท K-FOOD)์ ๋๋ค. ๊ทธ๋ฐ๋ฐ ๊ทธ ๊ด์ฌ์ ์ค์ ์ผํ์ผ๋ก ์ฎ๊ธฐ๋ ค๋ฉด ์ ๋ณด๊ฐ ํ๋งค์ฒ ยท ์์ ์ฝํ ์ธ ยท SNS ยท ํ๊ธฐ๋ก ์ ๋ถ ํฉ์ด์ ธ ์์ต๋๋ค.
SNS์์ ํ์ โ ์ด์ ์ ๋ณด ํ์ธ โ ํ๊ธฐ ๋น๊ต โ ๋งค์ฅ ์ ๋ณด ๋์กฐ โ ์ผ์ ยท๋์ ์ฌ๊ตฌ์ฑ.
๋ค์ฏ ๋จ๊ณ๋ฅผ ๋งค๋ฒ ๋ฐ๋ณตํด์ผ ํฉ๋๋ค. ์ ํ์ง๊ฐ ๋ง์์ง์๋ก ๋ถํ์ค์ฑ์ ์ปค์ง๊ณ , ๊ฒฐ์ ์ ๋ฆ์ด์ง๋๋ค.
๋ค๊ฐ ํด๋ดค์ผ๋, ๋๋.
| 01 DISCOVER ๋ฐ๊ฒฌ | 02 PLAN ๊ณํ | 03 NAVIGATE ์ด๋ |
| ๊ตญ๊ฐ๋ณ Trend Map์ผ๋ก K-ํธ๋ ๋๋ฅผ ๊ณจ๋ผ ๋ด๋๋ค | ์์ฒญ์ ๋ฐ์ํ AI ๋ง์ถคํ ์ฝ์ค๋ฅผ ๋ง๋ ๋ค | 3D ์ง๋์ ์ค๋ด ๊ธธ์ฐพ๊ธฐ๋ก ์ค์ ์ด๋๊น์ง ์๋ดํ๋ค |
์ด ์ ์ฅ์๋ ๊ทธ ์ธ ๋จ๊ณ๋ฅผ ์๋์ด ์ค์ ๋ก ๋ง์ง๋ ํ๋ฉด์ ๋๋ค.
4๊ฐ๊ตญ์ ๊ด์ฌ ์ฝํ
์ธ ์ ๋ธ๋๋๋ฅผ ๋ถ์ํด ๊ธฐ๋ณธ ์ฝ์ค๋ฅผ ์ถ์ฒํฉ๋๋ค.
๊ตญ๊ฐยท์ธ์ด๋ use-preference-store๊ฐ ditto-country ยท ditto-language ์ฟ ํค๋ก ๋ค๊ณ ์์ด
SSR ๋จ๊ณ์์ ์ด๋ฏธ ๊ฒฐ์ ๋ฉ๋๋ค โ hydration ๋ค์ ์ธ์ด๊ฐ ๋ค๋ฆ๊ฒ ๋ฐ๋์ง ์์ต๋๋ค.
/ai-course. ์ฝ์ค ํธ์ง์ฒ๋ผ ์ฌ๋ฌ Client Component๊ฐ ๊ณต์ ํ๋ ๋ค๋จ๊ณ ์์ฑ ์ํ๋
use-course-editor-store ํ ๊ณณ์ ๋ชจ์๋๋ค. ์์ฑ๋ ์ฝ์ค๋ ์ ์ฅํ๊ณ ๊ณต์ ํ ์ ์์ต๋๋ค.
three + @react-three/fiber + @react-three/drei๋ก 8๊ฐ ์ธต์ ํ๋์ ํ๋ฉด์ ๊ทธ๋ฆฝ๋๋ค.
| 01 | ์ถ์ฒ ์ฝ์ค๋ฅผ ์ธต๋ณ 3D๋ก ํ์ธ |
| 02 | ํ์ฌ ์์น์์ ๋ค์ ๋งค์ฅ๊น์ง ๊ธธ์ฐพ๊ธฐ |
| 03 | ์ธต์ด ๋ฐ๋์ด๋ ํ๋์ ๊ฒฝ๋ก๋ก ์ฐ๊ฒฐ |
๋ฐ๊ฒฌ ยท ๊ณํ ยท ์ด๋ ์ ๊ณผ์ ์ ํจ๊ปํ๋ ์น๊ตฌ.
CVAT๋ก ๋ผ๋ฒจ๋งํ ํต๋กยท์ถ์ ๊ตฌ๋ฅผ ๋ ธ๋ยท๊ฐ์ ๊ทธ๋ํ๋ก ๋ฐ๊พธ๊ณ A* ๋ก ํ์ํ ๊ฒฐ๊ณผ๋ฅผ, ์ด ์ ์ฅ์๊ฐ 2D ยท 3D๋ก ๊ทธ๋ฆฝ๋๋ค. ์ธต ๊ทธ๋ํ 8๊ฐ ยท ์ฅ์ ์์ฅ 147๊ณณ ยท ๋ฐฉ ํด๋ฆฌ๊ณค์ด ๊ทธ ์ ๋ ฅ์ ๋๋ค.
์ค๋ด ์ง๋ ์์ฅ(JSON 588KB)๊ณผ ์ธต ํ
์ค์ฒ(PNG 2.2MB)๋ ํ ๋ฒ ๋ง๋ค์ด์ง๋ฉด ๋ฐ๋์ง ์์ต๋๋ค.
CloudFront course-resource/*๋ก ๋ด๋ณด๋ด๊ณ 3์ฃผ์ง๋ฆฌ Cache-Control์ ๋ถ์์ต๋๋ค.
CDN ๋์
์ผ๋ก ์ด๋ฏธ์ง ๋ก๋ฉ โ51.7%(94.9ms โ 45.8ms), ๋์์ ์ฌ์ ์์ โ48.0%(222ms โ 150ms).
์ธต ํ
์ค์ฒ๋ next/image๊ฐ ์๋๋ผ three.js useTexture๊ฐ ๋ฐ์ต๋๋ค โ WebGL ํ
์ค์ฒ๋ผ
CORS ํค๋๊ฐ ํ์ํ๊ณ , CloudFront์ Managed-SimpleCORS ์ ์ฑ
์ด ๊ทธ๊ฒ์ ์ฑ์๋๋ค.
์์ธํ ๋ด์ฉ๊ณผ ํจ์ ์ ์๋ ์ค๋ด ์ง๋ ์ ์ ์์ฐ (CDN)์ ์์ต๋๋ค.
ํ๋จ + ์นด๋ฉ๋ผ๋ก ๋งค์ฅ ๋ก๊ณ ๋ฅผ ์ฐ์ผ๋ฉด POST /api/v1/ocr/locations/recognize๋ก ๋ณด๋ด๊ณ ,
/scan-map์ ์ ์ฒด์ธต 3D ์ง๋์ ํ์ผ๋ก ๋ด ์์น๋ฅผ ๋์๋๋ค.
์
๋ก๋ ์ ์ ์ฒ๋ฆฌ๋ก ํ๊ท ์ฉ๋์ โ85.0%(4.61MB โ 696KB) ์ค์ฌ ์๋ต ์๊ฐ๋ โ25.0%(1.60์ด โ 1.20์ด)๊ฐ ๋์ต๋๋ค.
๊ฑฐ๊ธฐ์ AI ์ฝ์ค ์์ฑํ๊ธฐ๋ฅผ ๋๋ฅด๋ฉด /ai-course?from=scan์ด ์ด๋ฆฌ๊ณ 1๋ฒ ์ฅ์๊ฐ ๋ฐฉ๊ธ ์ธ์ํ ๋งค์ฅ์ผ๋ก ์ฑ์์ง๋๋ค.
์ด ์์น ์ ๋ณด๋ ๊ทธ ๋ ํ๋ฉด์๋ง ๋จ๊ณ , ํญยทํค๋ยท๋ค๋ก๊ฐ๊ธฐ๋ก ๋๊ฐ๋ฉด ๋ฐ๋ก ์ง์๋๋ค.
next-intl๋ก ํ๊ตญ์ด ยท ์ค๊ตญ์ด ยท ์ผ๋ณธ์ด ยท ์์ด๋ฅผ ์ง์ํฉ๋๋ค. ๊ณ ์ UI ๋ฌธ๊ตฌ๋ messages/{locale}.json
๋ค ์นดํ๋ก๊ทธ์ ๊ฐ์ ํค๋ก ๋ค์ด๊ฐ์ผ ํ๊ณ , ํค ๊ตฌ์กฐ๊ฐ ์ด๊ธ๋๊ฑฐ๋ ๋ค๋ฅธ ์ธ์ด ํ์ผ์ ํ๊ตญ์ด๊ฐ ์์ด๋ฉด
pnpm test:i18n์ด ์ก์ต๋๋ค.
DITTO๋ ์ ์ฅ์ ์ ์ ๊ฑธ์ณ ์์ต๋๋ค.
| ์ ์ฅ์ | ๋ฌด์ |
|---|---|
| Ditto-FrontEnd (์ฌ๊ธฐ) | Next.js 16 App Router ยท Three.js 3D ์ง๋ ยท PWA |
| Ditto-BackEnd | Spring Boot 3 ยท Java 17 ยท MyBatis ยท Oracle |
| Ditto-AI | ๋๋ค 8์ข ยท RAG ์ถ์ฒ ํ์ดํ๋ผ์ธ ยท Azure OpenAI ยท Tavily |
๋ธ๋ผ์ฐ์ ๋ ํญ์ Next๋ง ๋ด
๋๋ค. next.config.mjs์ rewrites๊ฐ /api/*๋ฅผ ALB๋ก ๋๊ธฐ๋ฏ๋ก
CORS ์ค์ ์ด ํ์ ์๊ณ , ๋ฐฑ์๋ ์ฃผ์๋ ๋ฒ๋ค์ ๋
ธ์ถ๋์ง ์์ต๋๋ค.
Ditto / หdษชt.oส / โ ๋ฐ๋ผํ๋ค, ๋๋(me too). ๋ชจ๋๊ฐ ๋ฐ๋ผํ๊ณ ์ถ์ ๊ฒ.
๊ตญ๊ฐ๋ณ ํธ๋ ๋ ํ์
โ AI ๋ง์ถค ์ฝ์ค ์์ฑ
โ ์ฌ์ฉ์ ์ฝ์ค ์ปค์คํ
โ ๋ชจ๋ฐ์ผ ์ค๋ด ๊ธธ์ฐพ๊ธฐ
โ ์ฌํ์ ์ปค๋ฎค๋ํฐ ๊ณต์
ํ์ฌ ๋จ๊ณ๋ ๋ชจ๋ฐ์ผ PWA ํ๋ก ํธ์๋์ ๋๋ค. ํ๋ฉด์ 430px ์ฑ ์ ธ๋ก ๊ณ ์ ๋๊ณ , ํ ํ๋ฉด ์ค์นยทstandalone ํ์๋ฅผ ์ง์ํฉ๋๋ค.
| ์์ญ | ๊ธฐ์ | ์ฑ ์ |
|---|---|---|
| Framework | Next.js App Router | ๋ผ์ฐํ , ์๋ฒ ๋ ๋๋ง, ๋น๋ |
| UI | React | ์ปดํฌ๋ํธ์ ํ๋ฉด ์ํธ์์ฉ |
| Language | JavaScript | ์ ํ๋ฆฌ์ผ์ด์ ์ฝ๋ |
| Styling | Tailwind CSS | ๋ฐ์ํ UI์ ๋์์ธ ํ ํฐ |
| 3D | Three.js ยท @react-three/fiber ยท @react-three/drei | 8๊ฐ ์ธต ์ค๋ด ์ง๋์ ์ฝ์ค ๊ฒฝ๋ก ๋ ๋๋ง |
| HTTP | Axios | ๋ธ๋ผ์ฐ์ API ํต์ ๊ณตํต ์ธ์คํด์ค |
| Client state | Zustand | ์ฌ๋ฌ ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ๊ฐ ๊ณต์ ํ๋ ์ํ |
| Internationalization | next-intl | ์๋ฒ ๋ ๋๋ง๊ณผ ํด๋ผ์ด์ธํธ UI์ ํยท์คยท์ผยท์ ๊ณ ์ ๋ฌธ๊ตฌ |
| Package manager | pnpm | ์์กด์ฑ ๋ฐ ์ ๊ธ ํ์ผ ๊ด๋ฆฌ |
์ค์น๋ ์ ํํ ๋ฒ์ ์ package.json๊ณผ pnpm-lock.yaml์ ๊ธฐ์ค์ผ๋ก ํฉ๋๋ค.
์ปค๋ฐ ๋ฉ์์ง๋ ์๋ ํ์์ผ๋ก ์์ฑํฉ๋๋ค:
<ํ์
>:<๋ด์ฉ>
ex) feat: ๋ก๊ทธ์ธ ๊ธฐ๋ฅ ์ถ๊ฐ
| ํ์ | ์ค๋ช |
|---|---|
feat |
์๋ก์ด ๊ธฐ๋ฅ ์ถ๊ฐ |
fix |
๋ฒ๊ทธ ์์ |
docs |
๋ฌธ์ ์์ (README ๋ฑ) |
style |
์ฝ๋ ํฌ๋งทํ , ์ธ๋ฏธ์ฝ๋ก ๋๋ฝ ๋ฑ |
refactor |
์ฝ๋ ๋ฆฌํฉํ ๋ง (๊ธฐ๋ฅ ๋ณํ ์์) |
test |
ํ ์คํธ ์ฝ๋ ์ถ๊ฐ ๋๋ ์์ |
chore |
๊ธฐํ ๋ณ๊ฒฝ์ฌํญ (๋น๋ ์ค์ , ํจํค์ง ๋ฑ) |
| ๋ธ๋์น ์ด๋ฆ | ์ฉ๋ |
|---|---|
main |
๋ฐฐํฌ(Release)๊ฐ ์ด๋ฃจ์ด์ง๋ ์์ ์ ์ธ ์ฝ๋ |
dev |
๋ค์ ๋ฆด๋ฆฌ์ค๋ฅผ ์ค๋นํ๋ ๊ฐ๋ฐ ๋ธ๋์น |
๋ธ๋์น ๋ค์ด๋ฐ์ ์๋ ํ์์ผ๋ก ์์ฑํฉ๋๋ค:
<ํ์
>/<์ด์๋ฒํธ>
ex)feat/#23
| ํ์ | ์ค๋ช |
|---|---|
feat |
์๋ก์ด ๊ธฐ๋ฅ ์์ |
fix |
๋ฒ๊ทธ ์์ ์์ |
hotfix |
๊ธํ ์์ ์์ (๋ฐฐํฌ ํ ๋ฑ) |
refactor |
์ฝ๋ ๋ฆฌํฉํ ๋ง |
docs |
๋ฌธ์ ์์ |
chore |
๊ธฐํ ์์ (์ค์ , ํจํค์ง ๋ฑ) |
-
๊ธฐ๋ฅ ๊ฐ๋ฐ ์์
dev๋ธ๋์น์์ ์๋ก์ดfeat๋ธ๋์น๋ฅผ ์์ฑํ์ฌ ๊ฐ๋ฐ์ ์์ํฉ๋๋ค.
-
๊ธฐ๋ฅ ๊ฐ๋ฐ ๋ฐ ์ปค๋ฐ
feat๋ธ๋์น์์ ๊ธฐ๋ฅ์ ์์ฑํ๊ณ ์ปค๋ฐ์ ์งํํฉ๋๋ค.
-
์ฝ๋ ๋ฆฌ๋ทฐ ๋ฐ ๋ณํฉ
- ๊ฐ๋ฐ ์๋ฃ ํ,
feat๋ธ๋์น์์dev๋ธ๋์น๋ก PR(Pull Request)์ ์์ฑํ์ฌ ์ฝ๋ ๋ฆฌ๋ทฐ๋ฅผ ๋ฐ์ต๋๋ค. - ๋ฆฌ๋ทฐ๊ฐ ์๋ฃ๋๋ฉด
dev๋ธ๋์น์ ๋ณํฉํฉ๋๋ค.
- ๊ฐ๋ฐ ์๋ฃ ํ,
-
ํ ์คํธ
dev๋ธ๋์น์์ ๋ฐฐํฌ ์ ์ต์ข ๊ธฐ๋ฅ๋ค์ด ์์ ์ ์ผ๋ก ๋์ํ๋์ง ํ ์คํธํฉ๋๋ค.
-
๋ฐฐํฌ
- ํ
์คํธ๊ฐ ์๋ฃ๋๋ฉด
dev๋ธ๋์น๋ฅผmain๋ธ๋์น์ ๋ณํฉํ์ฌ ์ต์ข ๋ฐฐํฌ๋ฅผ ์งํํฉ๋๋ค.
- ํ
์คํธ๊ฐ ์๋ฃ๋๋ฉด
- Node.js 20.9 ์ด์์ ๋ง์กฑํ๋ LTS ๋ฒ์ ๊ถ์ฅ
- pnpm 9.12.3
git clone https://github.com/HDF-final/Ditto-FrontEnd.git
cd Ditto-FrontEnd
pnpm install
cp .env.example .env.local
pnpm dev๋ธ๋ผ์ฐ์ ์์ http://localhost:3000์ ์ฝ๋๋ค.
๊ฐ๋ฐ ์๋ฒ ์คํ:
pnpm dev์๋ฒ๊ฐ ์คํ ์ค์ธ ํฐ๋ฏธ๋์์ Ctrl+C๋ฅผ ๋๋ฅด๋ฉด ์ ์ ์ข
๋ฃ๋ฉ๋๋ค.
ํฐ๋ฏธ๋์ ๋ซ์๋๋ฐ ํ๋ก์ธ์ค๊ฐ ๋จ์ ์๋ค๋ฉด ๋จผ์ PID๋ฅผ ํ์ธํฉ๋๋ค.
lsof -nP -iTCP:3000 -sTCP:LISTENํ์๋ PID๋ง ์ข ๋ฃํฉ๋๋ค.
kill <PID>3000๋ฒ ํฌํธ๋ฅผ ๋ค๋ฅธ ํ๋ก์ธ์ค๊ฐ ์ฌ์ฉ ์ค์ผ ๋ ์์๋ก ๋ค๋ฅธ ํฌํธ๋ฅผ ์ฌ์ฉํ๋ ค๋ฉด:
pnpm exec next dev --port 3001Next.js App Router๋ src/app ์๋์ ํด๋๋ฅผ URL ๊ฒฝ๋ก๋ก ์ฌ์ฉํ๊ณ , ํด๋ ์์ page.js๊ฐ ์ค์ ํ์ด์ง๋ฅผ ๋
ธ์ถํฉ๋๋ค.
| URL | ํ์ผ | ์ฉ๋ |
|---|---|---|
/ |
src/app/page.js |
ํ |
/login |
src/app/login/page.js |
๋ก๊ทธ์ธ (UI ๊ฒ์ฆ๋ง, ์ฑ๊ณต ์ /๋ก ์ด๋) |
/signup |
src/app/signup/page.js |
ํ์๊ฐ์
(UI ๊ฒ์ฆ๋ง, ์ฑ๊ณต ์ /country๋ก ์ด๋) |
/country |
src/app/country/page.js |
๊ตญ๊ฐยท์ธ์ด ์ ํ (์ฑ๊ณต ์ /persona?lang=โฆ) |
/persona |
src/app/persona/page.js |
์ผํ ํ์
(ํ๋ฅด์๋) ์ ํ (์ฑ๊ณต ์ /๋ก ์ด๋) |
/ai-course |
src/app/ai-course/page.js |
AI ์ฝ์ค ๋ง๋ค๊ธฐ |
/scan-map |
src/app/scan-map/page.js |
OCR ๋ด ์์น 3D ์ง๋ |
/courses |
src/app/courses/page.js |
์ฝ์ค ๋ฆฌ์คํธ |
/community |
src/app/community/page.js |
์ฌํ์ ์ปค๋ฎค๋ํฐ |
/news |
src/app/news/page.js |
๋ด์คํผ๋ ๋ชฉ๋ก |
/news/[slug] |
src/app/news/[slug]/page.js |
๋ด์คํผ๋ ์์ธ (์ฌ๋ฌ๊ทธ ์กฐํ) |
/mypage |
src/app/mypage/page.js |
๋ง์ดํ์ด์ง |
์๋ฅผ ๋ค์ด /about ํ์ด์ง๋ฅผ ์ถ๊ฐํ๋ ค๋ฉด ๋ค์ ํ์ผ์ ๋ง๋ญ๋๋ค.
src/app/about/page.js
page.js์ layout.js๋ ๊ธฐ๋ณธ์ ์ผ๋ก Server Component์
๋๋ค. ์ํ, ์ด๋ฒคํธ, ๋ธ๋ผ์ฐ์ API ๋๋ Zustand๊ฐ ํ์ํ ์์ ์ปดํฌ๋ํธ์๋ง ํ์ผ ์๋จ์ "use client"๋ฅผ ์ ์ธํฉ๋๋ค.
Ditto-FrontEnd/
โโโ public/ # ์ด๋ฏธ์ง, ์์ด์ฝ ๋ฑ ์ ์ ํ์ผ
โโโ src/
โ โโโ app/ # App Router ํ์ด์ง์ ๋ ์ด์์
โ โ โโโ page.js # ํ
โ โ โโโ ai-course/page.js # AI ์ฝ์ค ๋ง๋ค๊ธฐ
โ โ โโโ news/page.js # ๋ด์คํผ๋
โ โ โโโ mypage/page.js # ๋ง์ดํ์ด์ง
โ โ โโโ login/page.js # ๋ก๊ทธ์ธ
โ โ โโโ signup/page.js # ํ์๊ฐ์
โ โ โโโ country/page.js # ๊ตญ๊ฐยท์ธ์ด ์ ํ
โ โ โโโ persona/page.js # ์ผํ ํ์
(ํ๋ฅด์๋) ์ ํ
โ โ โโโ layout.js # ์ ์ญ HTML ๋ ์ด์์ยทPWA viewport
โ โ โโโ manifest.js # ์น ์ฑ ๋งค๋ํ์คํธ
โ โ โโโ icon.js # PWA ์์ด์ฝ
โ โ โโโ globals.css # ์ ์ญ ์คํ์ผ๊ณผ Tailwind ํ ํฐ
โ โโโ components/
โ โ โโโ home/ # ํ ํ๋ฉด ์ ์ฉ ์น์
๊ณผ UI
โ โ โโโ ai-course/ # AI ์ฝ์ค ์์ฑยทํธ์ง ํ๋ฉด ์ ์ฉ UI
โ โ โโโ news/ # ๋ด์คํผ๋ ํ๋ฉด ์ ์ฉ UI
โ โ โโโ mypage/ # ๋ง์ดํ์ด์ง ํ๋ฉด ์ ์ฉ UI
โ โ โโโ auth/ # ๋ก๊ทธ์ธยท๊ฐ์
ยท๊ตญ๊ฐยทํ๋ฅด์๋ ์จ๋ณด๋ฉ UI
โ โ โโโ common/ # ๋๋ฉ์ธ์ ๋ฌถ์ด์ง ์๋ ์ฌ์ฌ์ฉ UI
โ โ โโโ layout/ # ์ฑ ์
ธ, ํค๋, ํ๋จ ํญ, PWA ๋ฑ๋ก
โ โโโ lib/
โ โ โโโ api/ # Axios ๊ณตํต ์ค์
โ โ โโโ pwa/ # ์ฑ ์์ด์ฝ ๋ ๋๋ฌ
โ โ โโโ fixtures/ # ํ๋ฉด ๊ฐ๋ฐ์ฉ ์ ์ ๋๋ฏธ ๋ฐ์ดํฐ
โ โ โโโ utils/ # ์์ ํจ์์ ๋ฒ์ฉ ์ ํธ๋ฆฌํฐ
โ โโโ stores/ # Zustand ์ ์ญ ํด๋ผ์ด์ธํธ ์ํ
โโโ .env.example # ๊ณต๊ฐ ๊ฐ๋ฅํ ํ๊ฒฝ๋ณ์ ์์
โโโ AGENTS.md # ์์ด์ ํธ ์์
๊ท์น
โโโ eslint.config.mjs
โโโ next.config.mjs
โโโ package.json
components/common์๋ ๋ฒํผ, ์ ํ๊ธฐ, ๋น ์ํ์ฒ๋ผ ์ฌ๋ฌ ๋๋ฉ์ธ์์ ๊ณต์ ํ๋ UI๋ฅผ ๋ก๋๋ค. components/layout์๋ ์ฑ ์
ธ, ํค๋, ํ๋จ ํญ์ฒ๋ผ ํ์ด์ง ๊ณจ๊ฒฉ์ ๋ง๋๋ UI๋ฅผ ๋ก๋๋ค. ํน์ ํ์ด์ง๋ ๋๋ฉ์ธ์์๋ง ์ฐ๋ ์ปดํฌ๋ํธ๋ components/home, components/ai-course, components/news, components/mypage, components/auth ์๋์ ๋จผ์ ๋ฐฐ์นํฉ๋๋ค.
lib/fixtures๋ API ์ฐ๋ ์ ํ๋ฉด์ ๊ตฌ์ฑํ๊ธฐ ์ํ ์ ์ ์ํ ๋ฐ์ดํฐ๋ง ๊ด๋ฆฌํฉ๋๋ค. ๊ตญ๊ฐ ๋ชฉ๋ก์ lib/fixtures/countries.js, ์ผํ ํ์
์ lib/fixtures/personas.js๋ฅผ ๋จ์ผ ์์ค๋ก ์๋๋ค. lib/utils๋ ๋ ์ง ํฌ๋งทํฐ, ๋ฌธ์์ด ๋ณํ, ๊ฐ ๊ฒ์ฆ์ฒ๋ผ React์ ๋ธ๋ผ์ฐ์ ์ํ์ ์์กดํ์ง ์๋ ์์ ์ ํธ๋ฆฌํฐ๋ฅผ ๊ด๋ฆฌํฉ๋๋ค. API endpoint๋ lib/api์์ ๋ฐฑ์๋ ๊ณ์ฝ์ด ํ์ ๋ ๋ค ์ถ๊ฐํฉ๋๋ค.
์ฝ์ค ํธ์ง์ฒ๋ผ ์ฌ๋ฌ Client Component๊ฐ ๊ณต์ ํ๋ ๋ค๋จ๊ณ ์์ฑ ์ํ๋ src/stores/use-course-editor-store.js์ ๋ก๋๋ค. ํ๋จ + ์นด๋ฉ๋ผ๋ก ๋ก๊ณ ๋ฅผ OCR ์ค์บํ๋ฉด POST /api/v1/ocr/locations/recognize๋ก ๊ฐํ ์ด๋ฏธ์ง๋ฅผ ๋ณด๋ด๊ณ , /scan-map์์ ์ ์ฒด์ธต 3D ์ง๋์ ์์ ์ง๋ ํ์ผ๋ก ๋ด ์์น๋ฅผ ํ์ํฉ๋๋ค. AI ์ฝ์ค ์์ฑํ๊ธฐ๋ฅผ ๋๋ฅด๋ฉด /ai-course?from=scan ๋น ์ฝ์ค ํธ์ง ํ๋ฉด์ด ์ด๋ฆฌ๊ณ 1๋ฒ ์ฅ์๊ฐ ๋ฐฉ๊ธ ์ธ์ํ ๋งค์ฅ์ผ๋ก ์ฑ์์ง๋๋ค. ์ด OCR ๋ด ์์น๋ ๊ทธ ๋ ํ๋ฉด์๋ง ์ ์ง๋๊ณ , ํญยทํค๋ยท๋ค๋ก๊ฐ๊ธฐ๋ก ๋๊ฐ๋ฉด ๋ฐ๋ก ์ง์๋๋ค. ๊ตญ๊ฐยท์ธ์ด๋ src/stores/use-preference-store.js์์ ์๋ก ๋
๋ฆฝ์ ์ผ๋ก ๊ด๋ฆฌํ๊ณ , SSR์์๋ ์ฝ์ ์ ์๋ ditto-country, ditto-language ์ฟ ํค์ ๋ณด์กดํฉ๋๋ค. ์ธ์ฆ ์ ๋ณด์ ์ธ์
๊ฐ์ ๋ธ๋ผ์ฐ์ ์ ์ฅ์์ ๋ฃ์ง ์์ต๋๋ค. ํ ํ๋ฉด ๋ด๋ถ์์๋ง ํ์ํ ์ํ๋ ์ ์ญ store๋ก ์ฌ๋ฆฌ์ง ์๊ณ ํด๋น ์ปดํฌ๋ํธ์ React state๋ก ๊ด๋ฆฌํฉ๋๋ค.
๊ตญ๊ฐยท์ธ์ด ์ค์ ์ฐ์ ์์๋ ๋ค์๊ณผ ๊ฐ์ต๋๋ค.
๋ก๊ทธ์ธ ์ฌ์ฉ์ โ GET /users/me์ DB ์ค์
๋น๋ก๊ทธ์ธ ์ฌ์ฉ์ โ ๊ตญ๊ฐยท์ธ์ด ์ฟ ํค
์ ์ฅ๋ ์ค์ ์์ โ ๋ธ๋ผ์ฐ์ ์ธ์ด + ๊ธฐ๋ณธ ๊ตญ๊ฐ KR
์ ์ญ ๋ ์ด์์์ด ์์ฒญ ์ฟ ํค์ Accept-Language๋ฅผ ์ฝ์ด Zustand provider์ ์ฒซ ์ํ์ <html lang>์ ํจ๊ป ์ด๊ธฐํํ๋ฏ๋ก, hydration ์ดํ ์ธ์ด๊ฐ ๋ค๋ฆ๊ฒ ๋ฐ๋๋ ํ์์ ๋ง์ต๋๋ค. ๊ตญ๊ฐ๋ฅผ ๋ฐ๊พธ๋ฉด ๊ทธ ๊ตญ๊ฐ์ ๊ธฐ๋ณธ ์ธ์ด๋ฅผ ํจ๊ป ์ ํํ๋, ์ฌ์ฉ์๊ฐ ์ธ์ด๋ฅผ ์ง์ ์ ํํ ๋ค์๋ ๊ตญ๊ฐ๋ฅผ ๋ฐ๊ฟ๋ ํด๋น ์ธ์ด๋ฅผ ์ ์งํฉ๋๋ค. ํค๋์์๋ ๊ตญ๊ฐ์ ํ๋ฉด ์ธ์ด๋ฅผ ๋ณ๋ ์ ํ๊ธฐ๋ก ์ ๊ณตํฉ๋๋ค.
์ ํ ์ธ์ด๋ Backend ๋์ ์ฝํ
์ธ ์๋ ๋์ผํ๊ฒ ์ ์ฉ๋ฉ๋๋ค. ๋ธ๋ผ์ฐ์ ์ ๋ชจ๋ Axios ์์ฒญ์ ๊ณตํต
src/lib/api/client.js ์ธํฐ์
ํฐ๊ฐ ditto-language ์ฟ ํค๋ฅผ ์ฝ์ด Accept-Language ํค๋์ ๋ฃ์ต๋๋ค.
๋ด์ค์ฒ๋ผ Server Component๊ฐ ์ง์ ํธ์ถํ๋ ์์ฒญ์ src/lib/api/server-language.js๊ฐ ์์ฒญ ์ฟ ํค๋ฅผ ์ฝ์ด
๊ฐ์ ํค๋๋ฅผ ์ ๋ฌํฉ๋๋ค. ์ง์ ๊ฐ์ ko, zh, ja, en์ด๋ฉฐ ๊ฐ์ด ์๊ฑฐ๋ ์๋ชป๋๋ฉด ko๋ฅผ ์ฌ์ฉํฉ๋๋ค.
ํธ์ถ๋ถ๊ฐ Accept-Language๋ฅผ ์ง์ ์ง์ ํ ๊ฒฝ์ฐ ๊ณตํต ๊ณ์ธต์ ํด๋น ๊ฐ์ ๋ฎ์ด์ฐ์ง ์์ต๋๋ค.
next-intl ๊ธฐ๋ฐ์ผ๋ก ํ๊ตญ์ด(ko), ์ค๊ตญ์ด(zh), ์ผ๋ณธ์ด(ja), ์์ด(en)๋ฅผ ์ง์ํฉ๋๋ค. ๋ฒ์ญ ์นดํ๋ก๊ทธ๋ messages/{locale}.json, ์์ฒญ๋ณ ์ธ์ด ๊ฒฐ์ ์ src/i18n/request.js, ์ง์ ์ธ์ด์ ๊ธฐ๋ณธ๊ฐ์ src/i18n/config.js์์ ๊ด๋ฆฌํฉ๋๋ค.
๊ณ ์ UI ๋ฌธ๊ตฌ๋ ๋ฉ์์ง ํค๋ก ๊ด๋ฆฌํ๋ฉฐ, ์ธ์ด ์ ํ ์ฟ ํค๊ฐ ๋ฐ๋๋ฉด ์๋ฒ ์ปดํฌ๋ํธ๊น์ง ๊ฐ์ ์ธ์ด๋ก ๋ค์ ๋ ๋๋งํฉ๋๋ค. ์ ๋ฌธ๊ตฌ๋ฅผ ์ถ๊ฐํ ๋๋ ๋ค ์นดํ๋ก๊ทธ์ ๋์ผํ ํค๋ฅผ ์ถ๊ฐํด์ผ ํฉ๋๋ค. ํค ๊ตฌ์กฐ ์ผ์น์ ๋ค๋ฅธ ์ธ์ด ํ์ผ์ ํ๊ตญ์ด ํผ์
์ pnpm test:i18n์ผ๋ก ๊ฒ์ฌํฉ๋๋ค.
์ด๋ฒ ๋จ๊ณ๋ ๋ฉ๋ด, ์ ๋ชฉ, ๋ฒํผ, ์ ๋ ฅ ์๋ด, ์ค๋ฅ์ ์ ๊ทผ์ฑ ๋ฌธ๊ตฌ ๊ฐ์ ํ๋ก ํธ ๊ณ ์ UI๋ง ํฌํจํฉ๋๋ค. ๋ด์คยท์ปค๋ฎค๋ํฐ ๊ฒ์๋ฌผยท์ฝ์ค๋ช ยทAI ์๋ต์ฒ๋ผ API๋ ์ฌ์ฉ์๊ฐ ๋ง๋๋ ๋์ ์ฝํ ์ธ ์ ์๋ ๋ฒ์ญ์ Azure Translator ์ฐ๋ ๋จ๊ณ์์ ๋ณ๋๋ก ์ฒ๋ฆฌํฉ๋๋ค. ๋ฐ๋ผ์ ํ์ฌ ๋จ๊ณ์๋ Azure ํค๊ฐ ํ์ํ์ง ์์ต๋๋ค.
์ธ์ฆยท์จ๋ณด๋ฉ ์ด๋ ์ ์ฑ :
/signup โ /country โ /persona?lang=โฆ โ /
/login โ /
๋ธ๋ผ์ฐ์ API ํต์ ์ ๊ณตํต Axios ์ธ์คํด์ค๋ฅผ ์ฌ์ฉํฉ๋๋ค.
import { apiClient } from "@/lib/api/client";
const { data } = await apiClient.get("/courses");axios.create()๋ฅผ ๊ธฐ๋ฅ ํ์ผ๋ง๋ค ์๋ก ๋ง๋ค์ง ์์ต๋๋ค.- API ๊ธฐ์ค ์ฃผ์๋
NEXT_PUBLIC_API_BASE_URL๋ก ๊ด๋ฆฌํฉ๋๋ค. - ํ์ผ ์
๋ก๋๋ฅผ ์ํด
Content-Type์ ์ ์ญ์ผ๋ก ๊ณ ์ ํ์ง ์์ต๋๋ค. - ์ธ์ฆ ๋ฐฉ์์ด ํ์ ๋๋ฉด ๊ณตํต ์ธํฐ์ ํฐ๋ ๊ฐ์ ํ์ผ์์ ๊ด๋ฆฌํฉ๋๋ค.
- ํ์ฌ ์ธ์คํด์ค๋ ๋ธ๋ผ์ฐ์ ํต์ ์ฉ์ ๋๋ค. Server Component์ ์๋ฒ ํต์ ์ ๋ฐฑ์๋ ๊ตฌ์กฐ ํ์ ํ ๋ณ๋ ์๋ฒ ๋ชจ๋๋ก ๋ถ๋ฆฌํฉ๋๋ค.
๋ธ๋ผ์ฐ์ ๋ ํญ์ ๊ฐ์ ์ค๋ฆฌ์ง์ /api/*๋ก ์์ฒญํ๊ณ , next.config.mjs์ rewrite๊ฐ ๋ฐฑ์๋๋ก ๋๊น๋๋ค. CORS ์ค์ ์์ด ๊ฐ๋ฐํ ์ ์๊ณ , ์ธ์
์ฟ ํค(JSESSIONID)๋ ๊ฐ์ ์ค๋ฆฌ์ง์ผ๋ก ์ค๊ฐ๋๋ค.
๋ธ๋ผ์ฐ์ /api/v1/... โ Next rewrite โ ${API_PROXY_TARGET}/api/v1/...
๋ฐฑ์๋ ์ฃผ์๊ฐ ๋ค๋ฅด๋ฉด .env.local์ API_PROXY_TARGET๋ง ๋ฐ๊ฟ๋๋ค. NEXT_PUBLIC_ ์ ๋์ฌ๊ฐ ์์ผ๋ฏ๋ก ๋ธ๋ผ์ฐ์ ๋ฒ๋ค์ ๋
ธ์ถ๋์ง ์์ต๋๋ค.
โ ๏ธ rewrite ํ๋ก์ ๊ธฐ๋ณธ ํ์์์์ 30์ด์ ๋๋ค30์ด๋ฅผ ๋๊ธฐ๋ API๋ ํ๋ก์๊ฐ ๋จผ์ ์์ผ์ ๋ซ์๋ฒ๋ฆฝ๋๋ค. ๊ทธ๋ฌ๋ฉด ๋ฐฑ์๋์๋
Broken pipe(ClientAbortException)๊ฐ ์ฐํ๊ณ , ๋ธ๋ผ์ฐ์ ๋ Spring์ด ์ค ์ ์๋Internal Server Error๋ฌธ์์ด์ ๋ฐ์ต๋๋ค. ์๋ต ๋ดํฌ({"success":false,...}) ํํ๊ฐ ์๋๋ฉด ์ด ๊ฒฝ์ฐ๋ฅผ ์์ฌํ์ธ์.AI ์ฝ์ค ์ถ์ฒ์ด 40~50์ด ๊ฑธ๋ฆฌ๋ฏ๋ก
next.config.mjs์์experimental.proxyTimeout์ 150์ด๋ก ๋๋ ค๋์์ต๋๋ค. ํด๋ผ์ด์ธํธ(axios) ํ์์์ 120์ด๋ณด๋ค ๊ธธ๊ฒ ์ก์, ์๊ฐ ์ด๊ณผ ํ๋จ์ ํญ์ axios๊ฐ ๋จผ์ ํ๊ณ ํ๋ฉด์ ์ฐ๋ฆฌ๊ฐ ๋ง๋ ์๋ด๊ฐ ๋จ๋๋ก ํ์ต๋๋ค.ํ์์์ ๊ณ์ธต์ ์ธ ๊ณณ์ ๋๋ค โ axios 120์ด < ํ๋ก์ 150์ด, ๊ทธ๋ฆฌ๊ณ ๋ฐฑ์๋. ์ ์ฅ๊ธฐ API๋ฅผ ๋ถ์ผ ๋ ์ด ์์๋ฅผ ์ ์งํ์ธ์.
POST /api/v1/ai/course-recommendations/chat ํ๋๋ก ์์ฑยท๋ค๋ฌ๊ธฐยท์ฌ์ถ์ฒ์ ๋ชจ๋ ์ฒ๋ฆฌํฉ๋๋ค.
| ํ๊ณ ์ถ์ ๊ฒ | ๋ณด๋ด๋ ๊ฐ |
|---|---|
| ๋ง์ถค ์ฝ์ค ์์ฑ (์ฒซ ์์ฒญ) | sessionId ์์ด message๋ง |
| ๋ํ๋ก ๋ค๋ฌ๊ธฐ / ์ฌ์ถ์ฒ | ์ง์ ์๋ต์ sessionId + message |
sessionId๋ ์๋ฒ๊ฐ ๋ฐ๊ธํฉ๋๋ค. ํด๋ผ์ด์ธํธ๊ฐ ๋ง๋ค์ง ์์ต๋๋ค.- ํ ํด์ 40์ด ์ํ ๊ฑธ๋ฆฌ๋ฏ๋ก ์ด ์์ฒญ๋ง ํ์์์์ 120์ด๋ก ๋ํ ๋ณด๋ ๋๋ค(๊ณตํต ์ธ์คํด์ค ๊ธฐ๋ณธ๊ฐ์ 15์ด).
- ์๋ต์
places[].navigationKey๋ฅผlib/navigation/course-routing-service.js์resolveCoursePlace()๋ก ์ค๋ด ์ง๋ ์ฅ์์ ๋งคํํฉ๋๋ค. ๋งค์นญ๋์ง ์๋ ํค๋ ๊ธธ์ฐพ๊ธฐ๊ฐ ๋ถ๊ฐ๋ฅํ๋ฏ๋ก ์ฝ์ค์์ ์ ์ธํฉ๋๋ค. - ๋ก๊ทธ์ธ ์ธ์ ์ด ํ์ํ ์๋ํฌ์ธํธ์ ๋๋ค. ๋ฏธ์ธ์ฆ ์ํ์์๋ 403์ด ์ค๊ณ ํ๋ฉด์ ๋ก๊ทธ์ธ ์๋ด๊ฐ ํ์๋ฉ๋๋ค.
K-์ปฌ์ฒ ํธ๋ ๋ ๋ด์คํผ๋ ์กฐํ API ์๋ํฌ์ธํธ:
| ์ฉ๋ | ๋ฉ์๋ | ์๋ํฌ์ธํธ | ํด๋ผ์ด์ธํธ ํจ์ | ์๋ฒ ํจ์ |
|---|---|---|---|---|
| ๋ด์คํผ๋ ๋ชฉ๋ก ์กฐํ | GET |
/api/v1/news?page={page}&size={size} |
getNewsFeeds() |
fetchNewsFeedsServer() |
| ๋ด์คํผ๋ ์์ธ ์กฐํ | GET |
/api/v1/news/{newsId} |
getNewsFeedById(id) |
getNewsDetailById(id) |
| ๊ฒ์ ์ ์ ์ฌ๋ฌ๊ทธ ์กฐํ | GET |
/api/v1/news/slug/{slug} |
getNewsFeedBySlug(slug) |
getNewsDetailBySlug(slug) |
| ์ฌ์ดํธ๋งต ๋ชฉ๋ก ์กฐํ | GET |
/api/v1/news/sitemap |
getNewsFeedsForSitemap() |
getNewsSitemap() |
- ์๋ฒ ์ปดํฌ๋ํธ(
src/app/news/page.js,src/app/news/[slug]/page.js,src/app/sitemap.js):lib/api/news.server.js๋ฅผ ํตํด SSR/SSG ๋ฐ ๋ฐฑ์๋ ๋ฏธ๊ฐ๋ ์ ์์ ํ ํด๋ฐฑ(fixtures) ์ฒ๋ฆฌ๋ฅผ ์ง์ํฉ๋๋ค. - ํด๋ผ์ด์ธํธ ์ปดํฌ๋ํธ:
lib/api/news.js๋ฅผ ํตํด ๋ธ๋ผ์ฐ์ ๊ณตํต Axios ์ธ์คํด์ค๋ก API ํต์ ์ ์ํํฉ๋๋ค.
ROLE_ADMIN ๊ณ์ ๋ง ์ ๊ทผํ ์ ์๋ ์กฐํ ์ ์ฉ ์ด์ ํ๋ฉด์
๋๋ค. ์ผ๋ฐ ์๋น์ค ํค๋ยทํธํฐ์ ๋ถ๋ฆฌ๋ ์ฌ์ด๋๋ฐ ๋ ์ด์์์ ์ฌ์ฉํ๋ฉฐ, ๋ฐฑ์๋๊ฐ S3์ ์ต์ ํธ๋ ๋ JSON์ ์ฝ์ด ์ ๊ณตํ ๊ฒฐ๊ณผ๋ฅผ ํ์ํฉ๋๋ค.
| ํ๋ฉด | ๊ฒฝ๋ก | ๋ด์ฉ |
|---|---|---|
| ์ด์ ๋์๋ณด๋ | /admin |
์ฐ์ถ๋ฌผ ์ํ, ๊ฒฝ๊ณ ์, ๊ตญ๊ฐ๋ณ TOP 10 ๋ฏธ๋ฆฌ๋ณด๊ธฐ |
| ๊ตญ๊ฐ๋ณ TOP 10 | /admin/trends/rankings |
ํ๊ตญยท์ค๊ตญยท์ผ๋ณธยท๋ฏธ๊ตญ ์ต์ข ์์ |
| ๊ตญ๊ฐ๋ณ ํ๋ณด๊ตฐ | /admin/trends/candidates |
๊ตญ๊ฐ๋ณ ๊ต์ฐจ ๊ฒ์ฆ ํ๋ณด๊ตฐ |
| YouTube ๊ธ์์น | /admin/trends/youtube |
์ต๊ทผ 7์ผ ์ ์ฒดยทํ๊ตญยท์ผ๋ณธยท๋ฏธ๊ตญ ๊ธ์์น ์ ํธ |
| ์น์ธ ๋๊ธฐ ์ฝ์ค ์ด์ | /admin/courses |
๋ฐฐ์น๊ฐ ๋ง๋ ์ ๋ฝ ์ฝ์ค ์ด์ โ ์นด๋๋ก ๋ณด๊ณ , ํ์ ์์ ํธ์ง |
ํธ๋ ๋ ํ๋ฉด์ 1์ฐจ ๋ฒ์๋ ์กฐํ์ ์๋ก๊ณ ์นจ๋ง ์ง์ํฉ๋๋ค. ์์ ์๋ ๋ณ๊ฒฝ, RDS ์ค๋ฒ๋ผ์ด๋, Lambda ์ฌ์คํ์ ํฌํจํ์ง ์์ต๋๋ค.
ditto-celeb-warm-2 ๋ฐฐ์น๊ฐ ์
๋ฝ ํ ๋ช
์ ์กฐ์ฌํด ๋งค์ฅ 3 + ์นดํ 1 + ์ฌ๊ฐ 1 ์ง๋ฆฌ ์ฝ์ค ์ด์์ ๋ง๋ค์ด Redis์ ํ๋ฃจ ๋์ ๋ก๋๋ค. ๊ด๋ฆฌ์๊ฐ ๋ณด๊ณ ์น์ธํด์ผ ์๋์๊ฒ ๋๊ฐ๋๋ค.
์ด์์ด ์นด๋๋ก ๋์ด์๊ณ , ์นด๋๋ฅผ ๋๋ฅด๋ฉด ํ์
ํธ์ง๊ธฐ๊ฐ ์ด๋ฆฝ๋๋ค. ํ๋ฉด์ ํต์งธ๋ก ๋ฐ๊พธ์ง ์๋ ๊ฒ์ ๊ด๋ฆฌ์๊ฐ ๋ชฉ๋ก์ผ๋ก ๋์์ค๋ ๋น์ฉ์ ์์ ๊ธฐ ์ํด์์
๋๋ค. Esc, ๋ฐฐ๊ฒฝ ํด๋ฆญ, ๋ซ๊ธฐ ๋ฒํผ์ผ๋ก ๋ซ์ต๋๋ค.
| ์ฉ๋ | ๋ฉ์๋ | ์๋ํฌ์ธํธ | ํด๋ผ์ด์ธํธ ํจ์ |
|---|---|---|---|
| ์ด์ ๋ชฉ๋ก ์กฐํ | GET |
/api/v1/admin/admin-courses |
getAdminCourses() |
| ์ค๋ ์คํ ์ํฉ ์กฐํ | GET |
/api/v1/admin/admin-courses/run |
getAdminCourseRun() |
| ์ด์ ์์ธ ์กฐํ | GET |
/api/v1/admin/admin-courses/{celebrity} |
getAdminCourse(name) |
| ์ฅ์ ์นดํ๋ก๊ทธ ์กฐํ | GET |
/api/v1/admin/admin-courses/places |
getAdminCoursePlaces() |
| ๋์ | ๋๋๊ทธ ๋๋ โโ ๋ฒํผ์ผ๋ก ์์ ๋ณ๊ฒฝ, โ ๋ก ์๋ฆฌ ๋นผ๊ธฐ |
| ๋งค์ฅ ๊ต์ฒด | ์ฐจ์์ ํ๋ณด์์, ๋๋ ๋ํ๋ ์ ์ฒด 147๊ณณ์์ ์ด๋ฆยท์นดํ ๊ณ ๋ฆฌยท์ธต์ผ๋ก ๊ฒ์ |
| ํ ์คํธ | ์๋ด ๋ฌธ์ฅ, ์ถ์ฒ ์ฌ์ , ๊ทผ๊ฑฐ ์ธ๋ฌผยท๋ธ๋๋ยท๋ฌธ์ฅ, ๊ทผ๊ฑฐ ๊ธฐ์ฌ URL |
| ์ฌ์ง | ์ฌ์ง URL, ์บก์
(์นด๋ฆฌ๋ ร ํ๋ผ๋ค), ์ถ์ฒ, ์ฌ์ง์ด ์ค๋ฆฐ ๊ธฐ์ฌ URL, ์ฌ์ง ์ข
๋ฅ |
| ๋ถ๋ฅ | ์๋ฆฌ์ ๋ถ๋ฅ(๋งค์ฅยท์์์ ยท์นดํยท์ฌ๊ฐ), ๊ทผ๊ฑฐ ์ข ๋ฅ(์ ๋ฝ ๊ทผ๊ฑฐยท๋์ ยท๊ด๋ฆฌ์) |
- ํธ์ง์ ์ ์ฅ๋์ง ์์ต๋๋ค. ๋ฐฑ์๋์๋ ๋๋ค์๋ ์ด์์ ๊ณ ์น๋ ์ฐฝ๊ตฌ๊ฐ ์์ง ์์ด(์ฝ๊ธฐ์ ์ญ์ ๋ง ์์ต๋๋ค) ํ์ ์ ๋ซ์ผ๋ฉด ์ฌ๋ผ์ง๋๋ค. ๊ฒฐ๊ณผ๋ JSON ๋ณต์ฌยท๋ด๋ ค๋ฐ๊ธฐ๋ก ๊ฐ์ ธ๊ฐ ์น์ธ ๋๋ค์ ๋๊น๋๋ค. ํธ์ง๊ธฐ ๋งจ ์๊ฐ ์ด ์ฌ์ค์ ๊ณ์ ๋งํด ์ค๋๋ค.
- ๋งค์ฅ์ ๊ฐ๋ฉด ๊ทผ๊ฑฐ๋ ๊ทธ๋๋ก ๋๊ณ ๊ทผ๊ฑฐ ์ฌ์ง๋ง ๋๋๋ค. ๋จ์ ๋ธ๋๋ ์ฌ์ง์ด ์ ๋งค์ฅ์ ๋ถ๋ ๊ฒ์ด ์ด ๋ฐฐ์น๊ฐ ์ค์ ๋ก ๋๋ ์ฌ๊ณ ์ ๋๋ค(์นด๋ฆฌ๋ ร ๋์ ค ๊ทผ๊ฑฐ ์๋ฆฌ์ MLB๊ฐ ๋ค์ด๊ฐ๋๋ฐ ์ฌ์ง๊ณผ ์บก์ ๋ง ๋์ ค์ด์์ต๋๋ค). ์ ๋งค์ฅ ์ด๋ฆ์ด ๊ทผ๊ฑฐ ๋ธ๋๋๋ฅผ ํ๊ณ ์์ผ๋ฉด ๊ทธ๋๋ก ๋ก๋๋ค.
- ๋ชฉ๋ก์ ๋จธ๋ฆฌ๋ง๋ง ์ต๋๋ค(์ธ๋ฌผยท์ํยท์ฝ์ค ๋ชจ์ยท๊ฒฝ๊ณ ์ยท๋จ์ TTL). ์ด์ ํ๋๊ฐ ์กฐ์ฌ ์๋ฌธ๊น์ง ๋ค๊ณ ์์ด ์์ญ KB๋ผ, ์ ๋ฌธ์ ์ฐ ์ด์๋ง ๋ฐ๋ก ๊ฐ์ ธ์ต๋๋ค.
- ๊ฒฝ๊ณ ๋ฅผ ํผ์น ์ฑ๋ก ๋งจ ์์ ๋ก๋๋ค. "์ฌ์ง ์์", "๊ทผ๊ฑฐ๋ ์๋๋ฐ ๊ทผ๊ฑฐ ์ฌ์ง์ ๋ชป ์ฐพ์๋ค", "์ฝ์ค ๋ชจ์์ด ์ด๊ธ๋ฌ๋ค"๊ฐ ๊ด๋ฆฌ์๊ฐ ์ด์์์ ์ ์ผ ๋จผ์ ๋ด์ผ ํ๋ ๊ฒ์ ๋๋ค.
- ์ฌ์ง์
next/image๊ฐ ์๋๋ผ<img>๋ก ๊ทธ๋ฆฝ๋๋ค. ์ฌ์ง์ด ๊ธฐ์ฌ์์ ์์ ํธ์คํธ๋ฅผ ๋ฏธ๋ฆฌ ์ ์ ์๋๋ฐnext.config.mjs์remotePatterns๋ ํธ์คํธ๋ฅผ ์ด๊ฑฐํด์ผ ํต๊ณผ์ํต๋๋ค. ์๋ณธ์ด ํซ๋งํฌ๋ฅผ ๋ง์ผ๋ฉด ๊ทธ ์๋ฆฌ๋ง ์ ์ต๋๋ค. - ๋ฐฐ์น ์คํ ์ํฉ(
/run)์ ๋ชป ์ฝ์ด๋ ๋ชฉ๋ก์ ๋ณด์ ๋๋ค โ ์ ๋ ๋ง ์ ํ๋๋ค.
ํ๋ก ํธ๋ง ๊ณ ์น ๋๋ ๋ฐฑ์๋๋ฅผ ๋ก์ปฌ์ ๋์ธ ํ์๊ฐ ์์ต๋๋ค. .env.local์ .env.example ๊ทธ๋๋ก ๋๋ฉด ๋ฉ๋๋ค.
NEXT_PUBLIC_API_BASE_URL=/api/v1
API_PROXY_TARGET=http://hdf-spring-alb-476185930.ap-northeast-2.elb.amazonaws.com
๋ธ๋ผ์ฐ์ ๋ localhost:3000/api/*๋ง ๋ถ๋ฅด๊ณ next.config.mjs์ rewrite๊ฐ ์๋ฒ ์ฌ์ด๋๋ก ALB์ ๋๊น๋๋ค. ๊ฐ์ ์ค๋ฆฌ์ง์ด๋ผ CORS๋ฅผ ์ ํ๋๋ค. localhost:3000์ ๋ฐฑ์๋ CORS ํ์ฉ ๋ชฉ๋ก์๋ ๋ค์ด ์์ผ๋ฏ๋ก, NEXT_PUBLIC_API_BASE_URL์ ์ ๋ ์ฃผ์๋ก ๋ฐ๊ฟ ๋ธ๋ผ์ฐ์ ๊ฐ ALB๋ฅผ ์ง์ ๋ถ๋ฅด๊ฒ ํด๋ ๋์ํฉ๋๋ค.
๊ด๋ฆฌ์ API๋ ์ธ์
์ธ์ฆ์ด๋ฏ๋ก pnpm dev๋ก ๋์ด ๋ค /login์์ ROLE_ADMIN ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธํด์ผ /admin/courses๊ฐ ๋ณด์
๋๋ค.
์ค๋ด ์ง๋ ์์ฅ(JSON 588KB)๊ณผ ์ธต ํ
์ค์ฒ(PNG 2.2MB)๋ ํ ๋ฒ ๋ง๋ค์ด์ง๋ฉด ๋ฐ๋์ง ์์ต๋๋ค. CloudFront ์ course-resource/* ๋์์ด S3 hdf-ditto-images ๋ฅผ ๋ด๋ณด๋ด๊ณ , ์ค๋ธ์ ํธ๋ง๋ค 3์ฃผ์ง๋ฆฌ Cache-Control ์ด ๋ถ์ด ์์ต๋๋ค.
https://d1bxld598du04o.cloudfront.net/course-resource
| ๋ฌด์ | ์ด๋ |
|---|---|
| ์ธต ๊ทธ๋ํ 8๊ฐ ยท ์ฅ์ ์์ฅ 147๊ณณ ยท ๋ฐฉ ํด๋ฆฌ๊ณค ยท ๋งค๋ํ์คํธ | {CDN}/navigation/v2/*.json |
| ์ธต ํ ์ค์ฒ 8์ฅ | {CDN}/maps/floor-*.png |
- ์ ์ฃผ์๊ฐ ์์ค์ ๊ธฐ๋ณธ๊ฐ์
๋๋ค(
src/lib/navigation/navigation-dataset.js).NEXT_PUBLIC_CDN_BASE๋ ๋ค๋ฅธ ๋ฒํทยท๋ฐฐํฌ๋ก ์ฎ๊ธธ ๋๋ง ์ค๋๋ค. NEXT_PUBLIC_CDN_BASE=๋ฅผ ๋น ๊ฐ์ผ๋ก ์ฃผ๋ฉดpublic/์์ ์ฌ๋ณธ์ผ๋ก ๋จ์ด์ง๋๋ค. ์ฌ๋ณธ์ ์ง์ฐ์ง ์์ ๊ฒ์ด CDN ์ด ๋งํ์ ๋์ ์์ ์ฅ์น์ด๊ณ , ๊ทธ ๊ฒฝ๋ก์๋ 3์ฃผ ์บ์ ํค๋๋ฅผ ๋ถ์ฌ ๋์ต๋๋ค(next.config.mjs).- ๊ธฐ๋ณธ๊ฐ์ด ํด๋ฐฑ์ด ์๋๋ผ CDN ์ธ ์ด์ .
NEXT_PUBLIC_*๋next build๊ฐ ๋ฒ๋ค์ ๋ฐ์ ๋ฃ๋ ๊ฐ์ธ๋ฐ, ๋ฐฐํฌ๋ ๋์ปค ์ด๋ฏธ์ง๋ฅผ ๋ง๋ค ๋.env๋ฅผ ์ ๋ฃ๊ณ ์ปจํ ์ด๋๋ฅผ ๋์ธ ๋๋ง ๋ฃ์ต๋๋ค(--env-file). ๊ทธ๋์ ์์ ๊ธฐ๋ณธ๊ฐ(public/์ฌ๋ณธ)์ด ๋ฐฐํฌ๋ณธ์ ๊ทธ๋๋ก ๋ฐํ, ๋ฐํ์.env์ CDN ์ฃผ์๊ฐ ์์ด๋ ์๋์ ๋ditto-global.com/navigation/v2/*.json์ผ๋ก 588KB ๋ฅผ ํผ ๊ฐ์ต๋๋ค. ๋ฐฐํฌ๋ณธ์ ๊ฐ์ ๋ฐ๊พธ๋ ค๋ฉดdocker build --build-arg NEXT_PUBLIC_CDN_BASE=...๋ก ๋น๋ํ ๋ ์ฃผ์ธ์(Dockerfile์ARG๊ฐ ์์ต๋๋ค). - ํ๊ท๋
pnpm test:navigation-assets๊ฐ ์ก์ต๋๋ค โ ํ๊ฒฝ๋ณ์ ์์ด ๋ค์ฌ์์ ๋ 8๊ฐ ์ธต์ ์์ฅยทํ ์ค์ฒ ์ฃผ์๊ฐ ์ ๋ถ CDN ์ ๊ฐ๋ฆฌํค๋์ง ๋ด ๋๋ค. - ๋ฐฑ์๋์
ditto.map-assets.base-url๊ณผ ๊ฐ์ ๊ณณ์ ๊ฐ๋ฆฌ์ผ์ผ ํฉ๋๋ค.GET /api/v1/places/navigation/assets๊ฐ ๊ฐ์ ์ฃผ์๋ฅผ ๋๋ ค์ฃผ๋ฏ๋ก, ์ฃผ์๋ฅผ ์ฎ๊ธธ ๋๋ ๋ ๊ณณ์ ๊ฐ์ด ๋ฐ๊ฟ๋๋ค. - ํฌ๋ก์ค ์ค๋ฆฌ์ง์ด๋ผ CloudFront
course-resource/*์Managed-SimpleCORS์๋ต ํค๋ ์ ์ฑ ์ด ๋ถ์ด ์์ต๋๋ค.Managed-CachingOptimized๋Origin์ ์๋ณธ์ ๋๊ธฐ์ง ์์, ๋ฒํท CORS ๋ง์ผ๋ก๋ ๋ธ๋ผ์ฐ์ ์์ ๋งํ๋๋ค. - ์ธต ํ
์ค์ฒ๋ three.js
useTexture๊ฐ ๋ฐ์ต๋๋ค(next/image๊ฐ ์๋๋๋ค). WebGL ํ ์ค์ฒ๋ผ CORS ํค๋๊ฐ ํ์ํ๊ณ , ์ ์ ์ฑ ์ด ๊ทธ๊ฒ์ ์ฑ์๋๋ค. - ์์ฅ์ ๋ค์ ๋ง๋ค๋ฉด ๊ฒฝ๋ก์
v2๋ฅผ ์ฌ๋ฆฌ์ธ์. ๊ฐ์ ํค๋ฅผ ๋ฎ์ด์ฐ๋ฉด ์ต๋ 3์ฃผ ๋์ ์ ํ์ผ์ด ๋๊ฐ๋๋ค.
Zustand๋ ๊ตญ๊ฐยท์ธ์ด, ๋ค๋จ๊ณ ์ฝ์ค ์์ฑ ์ํ, ์ ์ญ ๋ชจ๋ฌ์ฒ๋ผ ์ฌ๋ฌ Client Component๊ฐ ๊ณต์ ํด์ผ ํ๋ ์ํ์๋ง ์ฌ์ฉํฉ๋๋ค.
"use client";
import { usePreferenceStore } from "@/stores/use-preference-store";
const countryCode = usePreferenceStore((state) => state.countryCode);
const languageCode = usePreferenceStore((state) => state.languageCode);- ํ ์ปดํฌ๋ํธ์์๋ง ์ฌ์ฉํ๋ ๊ฐ์ React
useState๋ฅผ ์ฌ์ฉํฉ๋๋ค. - API ์๋ต ๋ชฉ๋ก ์ ์ฒด๋ฅผ Zustand์ ๋ถํ์ํ๊ฒ ๋ณต์ฌํ์ง ์์ต๋๋ค.
- ์ธ์ฆ ํ ํฐ์ Zustand persist๋
localStorage์ ์ ์ฅํ์ง ์์ต๋๋ค. - ๊ตญ๊ฐยท์ธ์ด ์ ์ฅ์๋ ์๋ฒ์์ ์ฟ ํค์ ๋ธ๋ผ์ฐ์ ์ธ์ด๋ก ์ด๊ธฐํํ๋ฉฐ, ๋ก๊ทธ์ธ๋ ๊ฒฝ์ฐ DB ๊ฐ์ผ๋ก ๋ฎ์ด์๋๋ค.
/country์์ ๊ตญ๊ฐ๋ ์ฝํ ์ธ ยทํธ๋ ๋ ์์ฅ, ์ธ์ด๋ ํ๋ฉด ํ์ ์ธ์ด๋ก ๋ณ๋ ์ ํํฉ๋๋ค.
- ์ง์ ๊ตญ๊ฐ:
KR,CN,JP,US - ์ง์ ์ธ์ด:
ko,zh,ja,en - ์ ์ฅ API:
PATCH /api/v1/users/me/preferences - ์ธ์
๋ณต์:
GET /api/v1/users/me - ๊ฒ์คํธ ์ ์ฅ:
ditto-country,ditto-language,ditto-language-manual์ฟ ํค - SSR ์ด๊ธฐํ: ์ฟ ํค๊ฐ ์์ผ๋ฉด
Accept-Language์ ๊ธฐ๋ณธ ๊ตญ๊ฐKR์ฌ์ฉ - ํค๋ ์ ํ๊ธฐ: ๊ตญ๊ฐ์ ํ๋ฉด ์ธ์ด๋ฅผ ๋ ๋ฆฝ์ ์ผ๋ก ๋ณ๊ฒฝ
- ๊ตญ๊ฐ ๋ณ๊ฒฝ: ์ธ์ด๋ฅผ ์ง์ ์ ํํ๊ธฐ ์ ์๋ ๊ตญ๊ฐ ๊ธฐ๋ณธ ์ธ์ด ์ ์ฉ, ์ง์ ์ ํํ ๋ค์๋ ์ธ์ด ์ ์ง
์ด๋ฒ 2์ฐจ์๋ ์ค์ ์ฐ์ ์์ ํตํฉ๊ณผ ์ฒซ ๋ ๋๋ง ๋ณต์๊น์ง ํฌํจํฉ๋๋ค. ํ๋ฉด ๊ณ ์ ๋ฌธ๊ตฌ ์ฌ์ , ์ค์ UI ์ ์ฒด ์ธ์ด ์ ํ, ๋์ ์ฝํ ์ธ ๋ฒ์ญ ๋ฐ DeepL ์ฐ๋์ ํ์ ๋จ๊ณ์ ๋๋ค.
Tailwind CSS 4๋ฅผ ์ฌ์ฉํ๋ฉฐ ์ ์ญ ๋์์ธ ํ ํฐ์ src/app/globals.css์ @theme์์ ๊ด๋ฆฌํฉ๋๋ค.
- ์ปดํฌ๋ํธ ์คํ์ผ์ Tailwind ์ ํธ๋ฆฌํฐ ํด๋์ค๋ฅผ ์ฐ์ ํฉ๋๋ค.
- ๋ฐ๋ณต๋๋ ์์ยท๊ฐ๊ฒฉยทํฐํธ ๊ฐ์ ๋์์ธ ํ ํฐ์ผ๋ก ์น๊ฒฉํฉ๋๋ค.
- ์์์ ์ ์ญ CSS๋ ๋ธ๋ผ์ฐ์ ๊ธฐ๋ณธ๊ฐ์ด๋ ๊ณตํต ํ ํฐ์ฒ๋ผ ๋ฒ์๊ฐ ๋ช ํํ ๋๋ง ์ถ๊ฐํฉ๋๋ค.
.env.example์ ํค ์ด๋ฆ๊ณผ ์์ ํ ์์๋ง ํฌํจํด Git์ ์ปค๋ฐํฉ๋๋ค. ์ค์ ๊ฐ์ Git์์ ์ ์ธ๋๋ .env.local์ ์์ฑํฉ๋๋ค.
NEXT_PUBLIC_API_BASE_URL=/api/v1
NEXT_PUBLIC_LOCAL_USER_ID=
API_PROXY_TARGET=http://hdf-spring-alb-476185930.ap-northeast-2.elb.amazonaws.com| ํค | ์ค๋ช |
|---|---|
NEXT_PUBLIC_API_BASE_URL |
๋ธ๋ผ์ฐ์ ๊ฐ ํธ์ถํ API ๊ธฐ์ค ๊ฒฝ๋ก. ๊ธฐ๋ณธ๊ฐ /api/v1. |
NEXT_PUBLIC_LOCAL_USER_ID |
๋ก์ปฌ Spring์ local ํ๋กํ์ ์๋์ ์ผ๋ก ์ํํ ๋๋ง ๋ฃ๋ X-User-Id. ๊ธฐ๋ณธ๊ฐ์ ๋น์ ๋ก๋๋ค. |
API_PROXY_TARGET |
Next๊ฐ /api/*๋ฅผ ๋๊ธธ ๋ฐฑ์๋ ์ฃผ์. ๊ธฐ๋ณธ๊ฐ์ ๋ฐฐํฌ๋ Backend ALB์
๋๋ค. |
NEXT_PUBLIC_ ์ ๋์ฌ๊ฐ ๋ถ์ ๊ฐ์ ๋ธ๋ผ์ฐ์ ๋ฒ๋ค์ ํฌํจ๋๋ฏ๋ก ๋น๋ฐ ํค๋ฅผ ๋ฃ์ผ๋ฉด ์ ๋ฉ๋๋ค.
NEXT_PUBLIC_LOCAL_USER_ID๊ฐ ๋ช
์๋ ๊ฐ๋ฐ ํ๊ฒฝ์์๋ง X-User-Id ํค๋๋ฅผ ์ ๋ฌํฉ๋๋ค. ๋ฐฐํฌ Backend๋ฅผ ํ๋ก์ํ ๋๋ ๋น์ ๋๊ณ ์ค์ JSESSIONID ์ธ์
์ผ๋ก ์ธ์ฆํฉ๋๋ค.
๋ธ๋ผ์ฐ์ ์ /api/* ์์ฒญ์ Next.js rewrite๋ฅผ ํตํด API_PROXY_TARGET์ผ๋ก ์ ๋ฌํฉ๋๋ค.
์ด ํ๋ก์ ํธ๋ pnpm๋ง ์ฌ์ฉํฉ๋๋ค. package-lock.json์ด๋ yarn.lock์ ๋ง๋ค์ง ์์ต๋๋ค.
# ํ๋ก๋์
์์กด์ฑ ์ถ๊ฐ
pnpm add <package>
# ๊ฐ๋ฐ ๋๊ตฌ ์ถ๊ฐ
pnpm add -D <package>
# ์ ๊ฑฐ
pnpm remove <package>
# ์ค๋๋ ํจํค์ง ํ์ธ
pnpm outdated
# ์ค์น ์ํ ์ฌํ
pnpm install --frozen-lockfilepackage.json๊ณผ pnpm-lock.yaml์ ํจ๊ป ์ปค๋ฐํ๊ณ ์ ๊ธ ํ์ผ์ ์ง์ ์์ ํ์ง ์์ต๋๋ค. ์ฃผ์ ๋ฒ์ ์
๊ทธ๋ ์ด๋๋ ๋ณ๊ฒฝ ์ฌํญ์ ํ์ธํ ๋ค ๋ณ๋ ์์
์ผ๋ก ์งํํฉ๋๋ค.
| ๋ช ๋ น์ด | ์ค๋ช |
|---|---|
pnpm dev |
3000๋ฒ ํฌํธ์์ ๊ฐ๋ฐ ์๋ฒ ์คํ |
pnpm lint |
ESLint ๊ฒ์ฌ |
pnpm build |
ํ๋ก๋์ ๋น๋ |
pnpm start |
๋น๋ ๊ฒฐ๊ณผ๋ฅผ 3000๋ฒ ํฌํธ์์ ์คํ |
pnpm check |
lint ํ build ์ฐ์ ๊ฒ์ฆ |
์น ์ฑ ๋งค๋ํ์คํธ, ํ ํ๋ฉด ์์ด์ฝ, ์๋น์ค ์์ปค, ์ค์น ๋ฐฐ๋๋ฅผ ํฌํจํฉ๋๋ค. ๋ฐ์คํฌํฑ์์๋ 430px ํฐ ์ ธ๋ก ๋ฏธ๋ฆฌ๋ณด๊ณ , ์ค์ ํฐ์์๋ ์ ์ฒด ํญ์ผ๋ก ํ์๋ฉ๋๋ค.
| ํญ๋ชฉ | ์์น |
|---|---|
| ๋งค๋ํ์คํธ | src/app/manifest.js โ /manifest.webmanifest |
| ์ฑ ์์ด์ฝ | /icons/192, /icons/512, apple-icon |
| ์๋น์ค ์์ปค | public/sw.js (PwaRegister๊ฐ ๋ฑ๋ก) |
| ํ๋จ ํญ | ํ ยท ์ฝ์ค ยท ๋ง๋ค๊ธฐ(+) ยท ๋ด์ค ยท ๋ง์ด |
์ค์น ๋ฐฉ๋ฒ:
- Chrome / Edge / Android: ํ ํ๋จ ๋ฐฐ๋์ "์ง๊ธ ์ค์นํ๊ธฐ", ๋๋ ๋ธ๋ผ์ฐ์ ๋ฉ๋ด์ ์ฑ ์ค์น
- iOS Safari: ๊ณต์ ๋ฒํผ โ ํ ํ๋ฉด์ ์ถ๊ฐ
์๋น์ค ์์ปค๋ ์ ์ ์์ฐ๊ณผ ํ ์
ธ์ ์บ์ํ๊ณ , /api/* ์์ฒญ์ ํญ์ ๋คํธ์ํฌ๋ก ๋ณด๋
๋๋ค. ์ธ์ฆ ํ ํฐ์ ์บ์ํ์ง ์์ต๋๋ค.












