๐ŸŽจ 3๋‹จ๊ณ„ : ์ปค๋ฎค๋‹ˆํ‹ฐ ๋„๋ฉ”์ธ ํ…Œํฌ์ŠคํŽ™ - 100-hours-a-week/7-team-ddb-wiki GitHub Wiki

๋ฌธ์„œ ๊ด€๋ฆฌ

์ž‘์„ฑ์ผ 2024-04-27
๋ฒ„์ „ 1.0
์ž‘์„ฑ์ž suzy.kang (๊ฐ•์ˆ˜์ง€)
๊ฒ€ํ† ์ž kevin
์ƒํƒœ ์ดˆ์•ˆ

๋ฐฐ๊ฒฝ (Background)

ํ”„๋กœ์ ํŠธ ๋ชฉํ‘œ (Objective)

  • ์‚ฌ์šฉ์ž ๊ฐ„ ๊ธฐ๋ก ๊ณต์œ  ๋ฐ ์†Œํ†ต ๊ธฐ๋Šฅ ์ œ๊ณต
  • ์ง๊ด€์ ์ธ ๊ธฐ๋ก ์ž‘์„ฑ ๋ฐ ์กฐํšŒ ์ธํ„ฐํŽ˜์ด์Šค ๊ตฌํ˜„
  • ํšจ์œจ์ ์ธ ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ๋ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ ๊ตฌ์ถ•

๋ชฉํ‘œ๊ฐ€ ์•„๋‹Œ ๊ฒƒ (Non-goals)

  • ์‹ค์‹œ๊ฐ„ ์ฑ„ํŒ… ๊ธฐ๋Šฅ
  • ๊ทธ๋ฃน ์ฑ„ํŒ…๋ฐฉ
  • ์‹ค์‹œ๊ฐ„ ์•Œ๋ฆผ

์„ค๊ณ„ ๋ฐ ๊ธฐ์ˆ  ์ž๋ฃŒ (Architecture and Technical Documentation)

์•„ํ‚คํ…์ฒ˜ ๊ฐœ์š” (Architecture Overview)

ํ”„๋ ˆ์ž„์›Œํฌ/๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ

  • Next.js (v15.3.0) + React (v19.1.0)
    • App Router ๊ธฐ๋ฐ˜ ๋ผ์šฐํŒ…
    • SSR/ISR ํ™œ์šฉ์œผ๋กœ SEO ์ตœ์ ํ™”
    • React 19์˜ use() ํ›… ํ™œ์šฉํ•œ ๋ฐ์ดํ„ฐ ํŽ˜์นญ

์ƒํƒœ ๊ด€๋ฆฌ

  • Zustand (v5.0.3)
    • ์ž‘์€ ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ๋กœ ๋ชจ๋ฐ”์ผ ์›น ์ตœ์ ํ™”
    • ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ํ˜ธํ™˜์„ฑ
    • ์ง๊ด€์ ์ธ API๋กœ ๋น ๋ฅธ ๊ฐœ๋ฐœ ๊ฐ€๋Šฅ

UI/์Šคํƒ€์ผ๋ง

  • Tailwind CSS (v4.1)
    • ์œ ํ‹ธ๋ฆฌํ‹ฐ ํผ์ŠคํŠธ ์ ‘๊ทผ์œผ๋กœ ๋น ๋ฅธ ๊ฐœ๋ฐœ
    • ์ปค์Šคํ…€ ํ…Œ๋งˆ๋กœ ๋ธŒ๋žœ๋“œ ๋””์ž์ธ ์‹œ์Šคํ…œ ๊ตฌ์ถ•
  • shadcn/ui
    • Tailwind ๊ธฐ๋ฐ˜ ์ปดํฌ๋„ŒํŠธ
    • ํ•„์š”ํ•œ ์ปดํฌ๋„ŒํŠธ๋งŒ ๊ฐ€์ ธ์™€ ๋ฒˆ๋“ค ํฌ๊ธฐ ์ตœ์ ํ™”

๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ

  • React use() + TanStack Query (v5)
    • ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ: use() ํ›…์œผ๋กœ SEO ์ตœ์ ํ™”
    • ํด๋ผ์ด์–ธํŠธ: TanStack Query๋กœ ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ
  • Zod (v3.24.3)
    • ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ ์—ฐ๋™
    • ์„œ๋ฒ„-ํด๋ผ์ด์–ธํŠธ ์Šคํ‚ค๋งˆ ๊ณต์œ 

ํผ ์ƒํƒœ ๊ด€๋ฆฌ

  • react-hook-form (v7.51.2)
    • ๊ฐ€๋ฒผ์šด ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ๋กœ ๋ชจ๋ฐ”์ผ ์นœํ™”์ 
    • Zod๊ณผ ํ†ตํ•ฉํ•ด ์ •๊ตํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ฐ€๋Šฅ
    • Server Action๊ณผ ๋ณ‘ํ–‰ ์‚ฌ์šฉ ๊ฐ€๋Šฅ (ํ•„์š” ์‹œ SSR + ๋น ๋ฅธ UX ๋ชจ๋‘ ํ™•๋ณด)

์ง€๋„ ์„œ๋น„์Šค

  • Kakao Maps SDK (v2.7.5)
    • ๊ตญ๋‚ด ์‚ฌ์šฉ์ž ์นœํ™”์  UI
    • ์ปค์Šคํ…€ ๋งˆ์ปค/ํด๋Ÿฌ์Šคํ„ฐ๋ง ์ง€์›

๋นŒ๋“œ ๋„๊ตฌ

  • Turbopack(v1.5.3) + webpack (v5)
    • Next.js 15 ๋‚ด์žฅ ๋นŒ๋“œ ๋„๊ตฌ ( Turbopack : ๊ฐœ๋ฐœ ํ™˜๊ฒฝ, Webpack : ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ )
    • ๋น ๋ฅธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๊ณผ HMR ์ง€์›

SSR / ISR / SSG ์ ์šฉ ๋ฒ”์œ„

  • /moments : SSR + CSR ํ˜ผํ•ฉ
    • SSR ์ ์šฉ ๋ถ€๋ถ„:
      • ์ดˆ๊ธฐ ๊ธฐ๋ก ๋ชฉ๋ก ๋กœ๋“œ (10๊ฐœ)
      • ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ฐ SEO ์ตœ์ ํ™”
    • CSR ์ ์šฉ ๋ถ€๋ถ„:
      • ๋ฌดํ•œ ์Šคํฌ๋กค (์ถ”๊ฐ€ 10๊ฐœ์”ฉ)
      • ์‹ค์‹œ๊ฐ„ ์ƒํ˜ธ์ž‘์šฉ
  • /moments/[id] : SSR + CSR ํ˜ผํ•ฉ
    • SSR ์ ์šฉ ๋ถ€๋ถ„:
      • ๊ธฐ๋ก ๊ธฐ๋ณธ ์ •๋ณด
      • ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ฐ SEO ์ตœ์ ํ™”
    • CSR ์ ์šฉ ๋ถ€๋ถ„:
      • ๋Œ“๊ธ€ ๋ฌดํ•œ ์Šคํฌ๋กค (10๊ฐœ์”ฉ)
      • ์‹ค์‹œ๊ฐ„ ์ƒํ˜ธ์ž‘์šฉ
  • /moments/new : CSR ์‚ฌ์šฉ
    • ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
    • ์‹ค์‹œ๊ฐ„ ํผ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
    • ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ์ค‘์‹ฌ
  • /moments/[id]/edit : CSR ์‚ฌ์šฉ
    • ์ด๋ฏธ์ง€ ์ˆ˜์ •
    • ์‹ค์‹œ๊ฐ„ ํผ ์œ ํšจ์„ฑ ๊ฒ€์ฆ
    • ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜ ์ค‘์‹ฌ
  • /users/[id] : SSR + CSR ํ˜ผํ•ฉ
    • SSR ์ ์šฉ ๋ถ€๋ถ„:
      • ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์ •๋ณด
      • ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ฐ SEO ์ตœ์ ํ™”
    • CSR ์ ์šฉ ๋ถ€๋ถ„:
      • ๊ธฐ๋ก ๋ชฉ๋ก ๋ฌดํ•œ ์Šคํฌ๋กค
      • ์‹ค์‹œ๊ฐ„ ์ƒํ˜ธ์ž‘์šฉ

์ฃผ์š” ํŽ˜์ด์ง€

/moments : ๋‚ด ๊ธฐ๋ก ๋ชฉ๋ก ํŽ˜์ด์ง€

  • ์ฃผ์š”๊ธฐ๋Šฅ:
    • ๊ธฐ๋ก ๋ชฉ๋ก ํ‘œ์‹œ (๋ฌดํ•œ ์Šคํฌ๋กค, 10๊ฐœ์”ฉ)
    • ๊ธฐ๋ก ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ํ‘œ์‹œ (์ž๋ฌผ์‡  ์•„์ด์ฝ˜)
    • ๋งจ ์œ„๋กœ ๋ฒ„ํŠผ (500px ์Šคํฌ๋กค ์‹œ ํ‘œ์‹œ)
  • ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ:
    • MomentList (๊ธฐ๋ก ๋ชฉ๋ก)
    • MomentCard (๊ธฐ๋ก ์นด๋“œ)
    • ScrollToTopButton (๋งจ ์œ„๋กœ ๋ฒ„ํŠผ)
  • ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹œ์ :
    • ํŽ˜์ด์ง€ ์ง„์ž… ์‹œ: GET /api/v1/users/me/moments?limit=10
    • ์Šคํฌ๋กค ์‹œ: GET /api/v1/users/me/moments?limit=10&cursor={cursor}
  • UI ์š”๊ตฌ์‚ฌํ•ญ:
    • ์‚ฌ์ง„ ์œ ๋ฌด์— ๋”ฐ๋ฅธ ๋ ˆ์ด์•„์›ƒ ์ฐจ๋ณ„ํ™”
    • ์ œ๋ชฉ: ์ตœ๋Œ€ 15์ž, ์ดํ›„ ๋ง์ค„์ž„ํ‘œ
    • ๋ณธ๋ฌธ: ์‚ฌ์ง„ ์žˆ์„ ๋•Œ 50์ž, ์—†์„ ๋•Œ 80์ž๊นŒ์ง€ ํ‘œ์‹œ
    • ์ฒซ ๋ฒˆ์งธ ์‚ฌ์ง„๋งŒ ํ‘œ์‹œ
    • ๋‚ ์งœ ๊ธฐ์ค€ ์ตœ์‹ ์ˆœ ์ •๋ ฌ

/moments/[id] : ๊ธฐ๋ก ์ƒ์„ธ ํŽ˜์ด์ง€

  • ์ฃผ์š”๊ธฐ๋Šฅ:
    • ๊ธฐ๋ก ์ƒ์„ธ ์ •๋ณด ํ‘œ์‹œ
    • ์ด๋ฏธ์ง€ ์Šฌ๋ผ์ด๋” (์ตœ๋Œ€ 3์žฅ, ์Šค์™€์ดํ”„/๋ฒ„ํŠผ)
    • ๋Œ“๊ธ€ ๋ชฉ๋ก ๋ฐ ์ž‘์„ฑ
    • ์ˆ˜์ •/์‚ญ์ œ ๊ธฐ๋Šฅ (๋ชจ๋‹ฌ ํ™•์ธ)
  • ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ:
    • MomentDetail (๊ธฐ๋ก ์ƒ์„ธ)
    • ImageSlider (์ด๋ฏธ์ง€ ์Šฌ๋ผ์ด๋”)
    • CommentList (๋Œ“๊ธ€ ๋ชฉ๋ก)
    • CommentForm (๋Œ“๊ธ€ ์ž‘์„ฑ)
  • ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹œ์ :
    • ํŽ˜์ด์ง€ ์ง„์ž… ์‹œ: ๊ธฐ๋ก ์ƒ์„ธ ์ •๋ณด
    • ์Šคํฌ๋กค ์‹œ: GET /api/v1/moments/{momentId}/comments?limit=10&cursor={cursor}
  • UI ์š”๊ตฌ์‚ฌํ•ญ:
    • ์ž‘์„ฑ์ž ํ”„๋กœํ•„ ์ด๋ฏธ์ง€
    • ์ œ๋ชฉ, ๋‚ ์งœ ํ‘œ์‹œ
    • ์ˆ˜์ •/์‚ญ์ œ ๋ฉ”๋‰ด (์  3๊ฐœ)
    • ์ด๋ฏธ์ง€ ์ธ๋””์ผ€์ดํ„ฐ (2์žฅ ์ด์ƒ)
    • ์žฅ์†Œ ์ด๋ฆ„ (ํด๋ฆญ ์‹œ ์žฅ์†Œ ์ƒ์„ธ)
    • ๋Œ“๊ธ€ ์ž…๋ ฅ์ฐฝ (ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ํฌํ•จ)
    • ๋Œ“๊ธ€ ๋ชฉ๋ก (ํ”„๋กœํ•„, ์ž‘์„ฑ์ผ, ๋ณธ๋ฌธ)
    • ์ž์‹ ์˜ ๋Œ“๊ธ€ ์‚ญ์ œ ์•„์ด์ฝ˜

/moments/new : ๊ธฐ๋ก ์ž‘์„ฑ ํŽ˜์ด์ง€

  • ์ฃผ์š”๊ธฐ๋Šฅ:
    • ๊ธฐ๋ก ์ž‘์„ฑ ํผ
    • ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ
    • ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ์„ค์ •
  • ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ:
    • MomentForm (๊ธฐ๋ก ์ž‘์„ฑ ํผ)
    • ImageUploader (์ด๋ฏธ์ง€ ์—…๋กœ๋“œ)
    • VisibilityToggle (๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ํ† ๊ธ€)
  • ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹œ์ :
    • ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์‹œ: POST /api/v1/gcs/signed-urls
    • ํผ ์ œ์ถœ ์‹œ: POST /api/v1/users/moments
  • UI ์š”๊ตฌ์‚ฌํ•ญ:
    • ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ํ† ๊ธ€
    • ์ž‘์„ฑ ์™„๋ฃŒ ๋ฒ„ํŠผ
    • ์ œ๋ชฉ ์ž…๋ ฅ ํ•„๋“œ
    • ๋ณธ๋ฌธ ์ž…๋ ฅ ํ•„๋“œ
    • ์žฅ์†Œ ํ‘œ์‹œ (์ˆ˜์ • ๋ถˆ๊ฐ€)

/moments/[id]/edit : ๊ธฐ๋ก ์ˆ˜์ • ํŽ˜์ด์ง€

  • ์ฃผ์š”๊ธฐ๋Šฅ:
    • ๊ธฐ๋ก ์ˆ˜์ • ํผ
    • ์ด๋ฏธ์ง€ ์ˆ˜์ •
    • ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ์„ค์ •
  • ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ:
    • MomentForm (๊ธฐ๋ก ์ˆ˜์ • ํผ)
    • ImageUploader (์ด๋ฏธ์ง€ ์ˆ˜์ •)
    • VisibilityToggle (๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ํ† ๊ธ€)
  • ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹œ์ :
    • ํŽ˜์ด์ง€ ์ง„์ž… ์‹œ: ๊ธฐ์กด ๊ธฐ๋ก ๋ฐ์ดํ„ฐ
    • ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์‹œ: POST /api/v1/gcs/signed-urls
    • ํผ ์ œ์ถœ ์‹œ: PATCH /api/v1/users/moments/{moment_id}
  • UI ์š”๊ตฌ์‚ฌํ•ญ:
    • ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ํ† ๊ธ€
    • ์ž‘์„ฑ ์™„๋ฃŒ ๋ฒ„ํŠผ
    • ์ œ๋ชฉ ์ˆ˜์ • ํ•„๋“œ
    • ๋ณธ๋ฌธ ์ˆ˜์ • ํ•„๋“œ
    • ์žฅ์†Œ ํ‘œ์‹œ (์ˆ˜์ • ๋ถˆ๊ฐ€)

/users/[id] : ๋‹ค๋ฅธ ์œ ์ € ํŽ˜์ด์ง€

  • ์ฃผ์š”๊ธฐ๋Šฅ:
    • ์œ ์ € ํ”„๋กœํ•„ ์ •๋ณด
    • ์œ ์ € ๊ธฐ๋ก ๋ชฉ๋ก
  • ์‚ฌ์šฉ ์ปดํฌ๋„ŒํŠธ:
    • UserProfile (์œ ์ € ํ”„๋กœํ•„)
    • MomentList (๊ธฐ๋ก ๋ชฉ๋ก)
  • ๋ฐ์ดํ„ฐ ๋กœ๋”ฉ ์‹œ์ :
    • ํŽ˜์ด์ง€ ์ง„์ž… ์‹œ: GET /api/v1/users/{user_id}
    • ์Šคํฌ๋กค ์‹œ: GET /api/v1/users/{user_id}/moments?limit=10&cursor={cursor}
  • UI ์š”๊ตฌ์‚ฌํ•ญ:
    • ํ”„๋กœํ•„ ์ด๋ฏธ์ง€
    • ๋‹‰๋„ค์ž„
    • ์†Œ๊ฐœ๊ธ€
    • ๊ธฐ๋ก ๋ชฉ๋ก (๋‚ด ๊ธฐ๋ก ๋ชฉ๋ก๊ณผ ๋™์ผ UI)
  • ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌ:
    • ํ”„๋กœํ•„ ์ด๋ฏธ์ง€ ์ตœ์ ํ™” (Next.js Image)
    • ๋‚ ์งœ ํฌ๋งทํŒ… (created_at, updated_at)

๊ณตํ†ต ์ปดํฌ๋„ŒํŠธ

MomentList : ๊ธฐ๋ก ๋ชฉ๋ก ์ปดํฌ๋„ŒํŠธ

  • ๊ฐœ์š”

    • ์—ญํ• : ๊ธฐ๋ก ๋ชฉ๋ก์„ ๋ฌดํ•œ ์Šคํฌ๋กค๋กœ ํ‘œ์‹œ
  • Props & Interface

    interface MomentListProps {
      moments: Array<{
        id: number;
        title: string;
        thumbnail?: string;
        images_count: number;
        is_public: boolean;
        created_at: string;
        place: {
          id: string;
          name: string;
        };
        location: string;
      }>;
      isLoading?: boolean;
      onLoadMore: () => Promise<void>;
      hasMore: boolean;
    }
    
  • ์Šคํƒ€์ผ๋ง

    • ๋ฆฌ์ŠคํŠธ ๋ ˆ์ด์•„์›ƒ
    • ๋ฌดํ•œ ์Šคํฌ๋กค ์ธ๋””์ผ€์ดํ„ฐ
    • ๋‚ ์งœ ํฌ๋งทํŒ… (yyyy.MM.dd)
  • Storybook

    • Default: ๊ธฐ๋ณธ ๋ชฉ๋ก
    • Loading: ๋กœ๋”ฉ ์ƒํƒœ
    • Empty: ๋นˆ ๋ชฉ๋ก
    • With Items: ๊ธฐ๋ก์ด ์žˆ๋Š” ์ƒํƒœ

MomentCard : ๊ธฐ๋ก ์นด๋“œ ์ปดํฌ๋„ŒํŠธ

  • ๊ฐœ์š”

    • ์—ญํ• : ๊ฐœ๋ณ„ ๊ธฐ๋ก ์ •๋ณด๋ฅผ ์นด๋“œ ํ˜•ํƒœ๋กœ ํ‘œ์‹œ
  • Props & Interface

    interface MomentCardProps {
      id: number;
      title: string;
      thumbnail?: string;
      images_count: number;
      is_public: boolean;
      created_at: string;
      place: {
        id: string;
        name: string;
      };
      location: string;
      onCardClick: (momentId: number) => void;
    }
    
  • ์Šคํƒ€์ผ๋ง

    • ์นด๋“œ ๋ ˆ์ด์•„์›ƒ
    • ์ด๋ฏธ์ง€ ๋น„์œจ
    • ๊ณต๊ฐœ/๋น„๊ณต๊ฐœ ์•„์ด์ฝ˜
  • Storybook

    • Default: ๊ธฐ๋ณธ ์นด๋“œ
    • With Image: ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋Š” ์ƒํƒœ
    • Private: ๋น„๊ณต๊ฐœ ์ƒํƒœ
    • Long Title: ๊ธด ์ œ๋ชฉ

ImageSlider : ์ด๋ฏธ์ง€ ์Šฌ๋ผ์ด๋” ์ปดํฌ๋„ŒํŠธ

  • ๊ฐœ์š”

    • ์—ญํ• : ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€๋ฅผ ์Šฌ๋ผ์ด๋”๋กœ ํ‘œ์‹œ
  • Props & Interface

    interface ImageSliderProps {
      images: string[];
      showIndicators?: boolean;
    }
    
  • ์Šคํƒ€์ผ๋ง

    • ์Šฌ๋ผ์ด๋” ๋ ˆ์ด์•„์›ƒ
    • ์ธ๋””์ผ€์ดํ„ฐ
    • ์Šค์™€์ดํ”„ ์• ๋‹ˆ๋ฉ”์ด์…˜
  • Storybook

    • Single Image: ๋‹จ์ผ ์ด๋ฏธ์ง€
    • Multiple Images: ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€
    • With Indicators: ์ธ๋””์ผ€์ดํ„ฐ ํ‘œ์‹œ

CommentList : ๋Œ“๊ธ€ ๋ชฉ๋ก ์ปดํฌ๋„ŒํŠธ

  • ๊ฐœ์š”

    • ์—ญํ• : ๋Œ“๊ธ€ ๋ชฉ๋ก์„ ๋ฌดํ•œ ์Šคํฌ๋กค๋กœ ํ‘œ์‹œ
  • Props & Interface

    interface CommentListProps {
      comments: Array<{
        id: number;
        user: {
          id: number;
          nickname: string;
          profile_image?: string;
        };
        content: string;
        created_at: string;
        is_owner: boolean;
      }>;
      isLoading?: boolean;
      onLoadMore: () => Promise<void>;
      hasMore: boolean;
      onDelete: (commentId: number) => Promise<void>;
    }
    
  • ์Šคํƒ€์ผ๋ง

    • ๋ฆฌ์ŠคํŠธ ๋ ˆ์ด์•„์›ƒ
    • ๋ฌดํ•œ ์Šคํฌ๋กค ์ธ๋””์ผ€์ดํ„ฐ
    • ๋‚ ์งœ ํฌ๋งทํŒ…
  • Storybook

    • Default: ๊ธฐ๋ณธ ๋ชฉ๋ก
    • Loading: ๋กœ๋”ฉ ์ƒํƒœ
    • Empty: ๋นˆ ๋ชฉ๋ก
    • With Items: ๋Œ“๊ธ€์ด ์žˆ๋Š” ์ƒํƒœ

CommentForm : ๋Œ“๊ธ€ ์ž‘์„ฑ ์ปดํฌ๋„ŒํŠธ

  • ๊ฐœ์š”

    • ์—ญํ• : ๋Œ“๊ธ€ ์ž‘์„ฑ ํผ
  • Props & Interface

    interface CommentFormProps {
      onSubmit: (content: string) => Promise<void>;
      userProfileImage?: string;
    }
    
  • ์Šคํƒ€์ผ๋ง

    • ํผ ๋ ˆ์ด์•„์›ƒ
    • ์ž…๋ ฅ ํ•„๋“œ
    • ์ œ์ถœ ๋ฒ„ํŠผ
  • Storybook

    • Default: ๊ธฐ๋ณธ ํผ
    • With Profile: ํ”„๋กœํ•„ ์ด๋ฏธ์ง€๊ฐ€ ์žˆ๋Š” ์ƒํƒœ
    • Loading: ์ œ์ถœ ์ค‘ ์ƒํƒœ

ScrollToTopButton : ๋งจ ์œ„๋กœ ๋ฒ„ํŠผ ์ปดํฌ๋„ŒํŠธ

  • ๊ฐœ์š”

    • ์—ญํ• : ํŽ˜์ด์ง€ ์ตœ์ƒ๋‹จ์œผ๋กœ ์Šคํฌ๋กคํ•˜๋Š” ๋ฒ„ํŠผ
  • Props & Interface

    interface ScrollToTopButtonProps {
      threshold?: number;
    }
    
  • ์Šคํƒ€์ผ๋ง

    • ๋ฒ„ํŠผ ๋””์ž์ธ
    • ์• ๋‹ˆ๋ฉ”์ด์…˜ ํšจ๊ณผ
  • Storybook

    • Default: ๊ธฐ๋ณธ ๋ฒ„ํŠผ
    • Visible: ํ‘œ์‹œ ์ƒํƒœ
    • Hidden: ์ˆจ๊น€ ์ƒํƒœ

TitleInput : ์ œ๋ชฉ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ

  • ๊ฐœ์š”

    • ์—ญํ• : ๊ธฐ๋ก ์ œ๋ชฉ์„ ์ž…๋ ฅํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ
  • Props & Interface

    interface TitleInputProps {
      value: string;
      onChange: (value: string) => void;
      placeholder?: string;
      maxLength?: number;
      error?: string;
    }
    
  • ์Šคํƒ€์ผ๋ง

    • ์ž…๋ ฅ ํ•„๋“œ ๋ ˆ์ด์•„์›ƒ
    • ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์Šคํƒ€์ผ
    • ๊ธ€์ž ์ˆ˜ ์ œํ•œ ํ‘œ์‹œ
  • Storybook

    • Default: ๊ธฐ๋ณธ ์ž…๋ ฅ ํ•„๋“œ
    • With Error: ์—๋Ÿฌ ์ƒํƒœ
    • With Max Length: ์ตœ๋Œ€ ๊ธธ์ด ์ œํ•œ
    • With Placeholder: ํ”Œ๋ ˆ์ด์Šคํ™€๋”

ContentInput : ๋ณธ๋ฌธ ์ž…๋ ฅ ์ปดํฌ๋„ŒํŠธ

  • ๊ฐœ์š”

    • ์—ญํ• : ๊ธฐ๋ก ๋ณธ๋ฌธ์„ ์ž…๋ ฅํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ
  • Props & Interface

    interface ContentInputProps {
      value: string;
      onChange: (value: string) => void;
      placeholder?: string;
      maxLength?: number;
      error?: string;
      rows?: number;
    }
    
  • ์Šคํƒ€์ผ๋ง

    • ํ…์ŠคํŠธ ์˜์—ญ ๋ ˆ์ด์•„์›ƒ
    • ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์Šคํƒ€์ผ
    • ๊ธ€์ž ์ˆ˜ ์ œํ•œ ํ‘œ์‹œ
  • Storybook

    • Default: ๊ธฐ๋ณธ ํ…์ŠคํŠธ ์˜์—ญ
    • With Error: ์—๋Ÿฌ ์ƒํƒœ
    • With Max Length: ์ตœ๋Œ€ ๊ธธ์ด ์ œํ•œ
    • With Placeholder: ํ”Œ๋ ˆ์ด์Šคํ™€๋”
    • With Custom Rows: ์ปค์Šคํ…€ ํ–‰ ์ˆ˜

PlaceMomentList : ์žฅ์†Œ๋ณ„ ๊ธฐ๋ก ๋ชฉ๋ก ์ปดํฌ๋„ŒํŠธ

  • ๊ฐœ์š”

    • ์—ญํ• : ํŠน์ • ์žฅ์†Œ์— ๋Œ€ํ•œ ๊ธฐ๋ก ๋ชฉ๋ก์„ ํ‘œ์‹œ
  • Props & Interface

    interface PlaceMomentListProps {
      placeId: string;
      moments: Array<{
        id: number;
        title: string;
        thumbnail?: string;
        images_count: number;
        is_public: boolean;
        created_at: string;
        user: {
          id: number;
          nickname: string;
          profile_image?: string;
        };
        location: string;
      }>;
      isLoading?: boolean;
      onLoadMore: () => Promise<void>;
      hasMore: boolean;
    }
    
  • ์Šคํƒ€์ผ๋ง

    • ๋ฆฌ์ŠคํŠธ ๋ ˆ์ด์•„์›ƒ
    • ๋ฌดํ•œ ์Šคํฌ๋กค ์ธ๋””์ผ€์ดํ„ฐ
    • ๋‚ ์งœ ํฌ๋งทํŒ…
  • Storybook

    • Default: ๊ธฐ๋ณธ ๋ชฉ๋ก
    • Loading: ๋กœ๋”ฉ ์ƒํƒœ
    • Empty: ๋นˆ ๋ชฉ๋ก
    • With Items: ๊ธฐ๋ก์ด ์žˆ๋Š” ์ƒํƒœ

์ปดํฌ๋„ŒํŠธ ๊ฐ„ ๊ด€๊ณ„

  • MomentList:
    • ์—ญํ• : ๊ธฐ๋ก ๋ชฉ๋ก ํ‘œ์‹œ
    • ๊ด€๊ณ„: MomentCard๋ฅผ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋กœ ํฌํ•จ
  • MomentDetail:
    • ์—ญํ• : ๊ธฐ๋ก ์ƒ์„ธ ์ •๋ณด ํ‘œ์‹œ
    • ๊ด€๊ณ„: ImageSlider, CommentList, CommentForm๊ณผ ์—ฐ๋™
  • MomentForm:
    • ์—ญํ• : ๊ธฐ๋ก ์ž‘์„ฑ/์ˆ˜์ • ํผ
    • ๊ด€๊ณ„:
      • TitleInput, ContentInput์„ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋กœ ํฌํ•จ
      • ImageUploader, VisibilityToggle๊ณผ ์—ฐ๋™
  • PlaceMomentList:
    • ์—ญํ• : ์žฅ์†Œ๋ณ„ ๊ธฐ๋ก ๋ชฉ๋ก ํ‘œ์‹œ
    • ๊ด€๊ณ„: MomentCard๋ฅผ ์ž์‹ ์ปดํฌ๋„ŒํŠธ๋กœ ํฌํ•จ

์ƒํƒœ ๊ด€๋ฆฌ ์ „๋žต

  • Local State (React useState):
    • MomentList: ๋ฌดํ•œ ์Šคํฌ๋กค ์ƒํƒœ
    • ImageSlider: ํ˜„์žฌ ์ด๋ฏธ์ง€ ์ธ๋ฑ์Šค
    • CommentForm: ์ž…๋ ฅ๊ฐ’ ์ƒํƒœ
  • Global State (Zustand):
    • momentStore:
      • ์ƒํƒœ: moments, currentMoment, isLoading
      • ์•ก์…˜: fetchMoments, createMoment, updateMoment, deleteMoment
    • commentStore:
      • ์ƒํƒœ: comments, isLoading
      • ์•ก์…˜: fetchComments, createComment, deleteComment
    • placeStore:
      • ์ƒํƒœ: placeMoments, isLoading
      • ์•ก์…˜: fetchPlaceMoments, loadMorePlaceMoments
  • Server Cache State (TanStack Query):
    • ๊ธฐ๋ก ๋ชฉ๋ก ์บ์‹ฑ
    • ๊ธฐ๋ก ์ƒ์„ธ ์ •๋ณด ์บ์‹ฑ
    • ๋Œ“๊ธ€ ๋ชฉ๋ก ์บ์‹ฑ

API ์—ฐ๋™ (API Integration)

  • ํ˜ธ์ถœํ•  ๋ฐฑ์—”๋“œ API ๋ชฉ๋ก
    • GET /api/v1/users/me/moments: ๋ณธ์ธ ๊ธฐ๋ก ๋ชฉ๋ก ์กฐํšŒ
    • GET /api/v1/users/{user_id}/moments: ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž ๊ธฐ๋ก ๋ชฉ๋ก ์กฐํšŒ
    • GET /api/v1/users/{user_id}: ๋‹ค๋ฅธ ์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์กฐํšŒ
    • POST /api/v1/users/moments: ๊ธฐ๋ก ์ž‘์„ฑ
    • PATCH /api/v1/users/moments/{moment_id}: ๊ธฐ๋ก ์ˆ˜์ •
    • DELETE /api/v1/users/moments/{moment_id}: ๊ธฐ๋ก ์‚ญ์ œ
    • POST /api/v1/gcs/signed-urls: ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ URL ๋ฐœ๊ธ‰
    • GET /api/v1/moments/{momentId}/comments: ๋Œ“๊ธ€ ์กฐํšŒ
    • POST /api/v1/moments/{moment_id}/comments: ๋Œ“๊ธ€ ์ž‘์„ฑ
    • DELETE /api/v1/moments/{moment_id}/comments/{comment_id}: ๋Œ“๊ธ€ ์‚ญ์ œ
  • API ํ˜ธ์ถœ ์ฒ˜๋ฆฌ
    • ๋ฌดํ•œ ์Šคํฌ๋กค ๊ตฌํ˜„
    • ์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์ตœ์ ํ™”
    • ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐ ์žฌ์‹œ๋„
    • ์บ์‹œ ๊ด€๋ฆฌ

์„ฑ๋Šฅ ์ตœ์ ํ™”

  • ์ด๋ฏธ์ง€ ์ตœ์ ํ™”
    • Next.js Image ์ปดํฌ๋„ŒํŠธ ์‚ฌ์šฉ
    • WebP ํฌ๋งท ์‚ฌ์šฉ
    • ์ ์ ˆํ•œ ์ด๋ฏธ์ง€ ํฌ๊ธฐ ์„ค์ •
  • ๋ฌดํ•œ ์Šคํฌ๋กค ์ตœ์ ํ™”
    • ๊ฐ€์ƒํ™” ์Šคํฌ๋กค ์ ์šฉ
    • ๋ฐ์ดํ„ฐ ํ”„๋ฆฌํŽ˜์นญ
  • ๋ฒˆ๋“ค ํฌ๊ธฐ ์ตœ์ ํ™”
    • ์ฝ”๋“œ ์Šคํ”Œ๋ฆฌํŒ…
    • ๋™์  ์ž„ํฌํŠธ

์œ ํšจ์„ฑ ๊ฒ€์ฆ (Validation)

์ž…๋ ฅ ํ•„๋“œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ

์ œ๋ชฉ ์ž…๋ ฅ ํ•„๋“œ

  • ์ตœ์†Œ ๊ธธ์ด: 2์ž
  • ์ตœ๋Œ€ ๊ธธ์ด: 50์ž
  • ํ•„์ˆ˜ ์ž…๋ ฅ: true
  • ๊ธฐ๋ณธ ๊ฐ’: (์œ ์ € ์ด๋ฆ„)์˜ ๊ธฐ๋ก
  • ์ œํ•œ ์‚ฌํ•ญ:
    • ๊ณต๋ฐฑ๋งŒ์œผ๋กœ ๊ตฌ์„ฑ ๋ถˆ๊ฐ€
    • ํŠน์ˆ˜๋ฌธ์ž ์‚ฌ์šฉ ์ œํ•œ (์ด๋ชจ์ง€, HTML ํƒœ๊ทธ ๋“ฑ)
  • ์—๋Ÿฌ ๋ฉ”์‹œ์ง€:
    • ํ•„์ˆ˜ ์ž…๋ ฅ: "์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
    • ์ตœ์†Œ ๊ธธ์ด ๋ฏธ๋‹ฌ: "์ œ๋ชฉ์€ ์ตœ์†Œ 2์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"
    • ์ตœ๋Œ€ ๊ธธ์ด ์ดˆ๊ณผ: "์ œ๋ชฉ์€ 50์ž๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
    • ๊ณต๋ฐฑ๋งŒ ์ž…๋ ฅ: "์ œ๋ชฉ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
    • ํŠน์ˆ˜๋ฌธ์ž ์‚ฌ์šฉ: "ํŠน์ˆ˜๋ฌธ์ž๋Š” ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"

๋ณธ๋ฌธ ์ž…๋ ฅ ํ•„๋“œ

  • ์ตœ์†Œ ๊ธธ์ด: 1์ž
  • ์ตœ๋Œ€ ๊ธธ์ด: 2200์ž
  • ํ•„์ˆ˜ ์ž…๋ ฅ: true
  • ์ œํ•œ ์‚ฌํ•ญ:
    • ๊ณต๋ฐฑ๋งŒ์œผ๋กœ ๊ตฌ์„ฑ ๋ถˆ๊ฐ€
    • XSS ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ HTML ํƒœ๊ทธ ํ•„ํ„ฐ๋ง
  • ์—๋Ÿฌ ๋ฉ”์‹œ์ง€:
    • ํ•„์ˆ˜ ์ž…๋ ฅ: "๋ณธ๋ฌธ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
    • ์ตœ์†Œ ๊ธธ์ด ๋ฏธ๋‹ฌ: "๋ณธ๋ฌธ์€ ์ตœ์†Œ 1์ž ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"
    • ์ตœ๋Œ€ ๊ธธ์ด ์ดˆ๊ณผ: "๋ณธ๋ฌธ์€ 2200์ž๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
    • ๊ณต๋ฐฑ๋งŒ ์ž…๋ ฅ: "๋ณธ๋ฌธ์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"

๋Œ“๊ธ€ ์ž…๋ ฅ ํ•„๋“œ

  • ์ตœ๋Œ€ ๊ธธ์ด: 200์ž
  • ํ•„์ˆ˜ ์ž…๋ ฅ: true
  • ์ œํ•œ ์‚ฌํ•ญ:
    • ๊ณต๋ฐฑ๋งŒ์œผ๋กœ ๊ตฌ์„ฑ ๋ถˆ๊ฐ€
    • XSS ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ HTML ํƒœ๊ทธ ํ•„ํ„ฐ๋ง
  • ์—๋Ÿฌ ๋ฉ”์‹œ์ง€:
    • ํ•„์ˆ˜ ์ž…๋ ฅ: "๋Œ“๊ธ€์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"
    • ์ตœ๋Œ€ ๊ธธ์ด ์ดˆ๊ณผ: "๋Œ“๊ธ€์€ 200์ž๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
    • ๊ณต๋ฐฑ๋งŒ ์ž…๋ ฅ: "๋Œ“๊ธ€์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”"

์ด๋ฏธ์ง€ ์—…๋กœ๋“œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ

์ด๋ฏธ์ง€ ํŒŒ์ผ

  • ์ตœ๋Œ€ ๊ฐœ์ˆ˜: 3๊ฐœ
  • ํŒŒ์ผ ํ˜•์‹: jpg, jpeg, png, webp
  • ์ตœ๋Œ€ ํฌ๊ธฐ: 5MB
  • ์ œํ•œ ์‚ฌํ•ญ:
    • ์ด๋ฏธ์ง€ ๋น„์œจ: 1:1
  • ์—๋Ÿฌ ๋ฉ”์‹œ์ง€:
    • ์ตœ๋Œ€ ๊ฐœ์ˆ˜ ์ดˆ๊ณผ: "์ด๋ฏธ์ง€๋Š” ์ตœ๋Œ€ 3๊ฐœ๊นŒ์ง€ ์—…๋กœ๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค"
    • ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ˜•์‹: "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์ด๋ฏธ์ง€ ํ˜•์‹์ž…๋‹ˆ๋‹ค"
    • ์ตœ๋Œ€ ํฌ๊ธฐ ์ดˆ๊ณผ: "์ด๋ฏธ์ง€ ํฌ๊ธฐ๋Š” 5MB๋ฅผ ์ดˆ๊ณผํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"
โš ๏ธ **GitHub.com Fallback** โš ๏ธ