Skip to content

Latest commit

ย 

History

History
188 lines (140 loc) ยท 15.1 KB

File metadata and controls

188 lines (140 loc) ยท 15.1 KB

CLAUDE.md

Finders โ€” ํ•„๋ฆ„์นด๋ฉ”๋ผ ํ˜„์ƒ์†Œ ํƒ์ƒ‰ยทํ˜„์ƒ/์ธํ™” ์ฃผ๋ฌธยท์‚ฌ์ง„ ํ”ผ๋“œ ์„œ๋น„์Šค. React SPA๋ฅผ Capacitor๋กœ ๊ฐ์‹ผ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ์•ฑ (Web + Android + iOS). ๋ชจ๋ฐ”์ผ ์ „์šฉ UI (max-width 480px).

์ด ๋ฌธ์„œ๋Š” ์ฝ”๋“œ๋งŒ ์ฝ์–ด์„œ๋Š” ์•Œ ์ˆ˜ ์—†๋Š” ๊ทœ์น™ยท๊ด€์Šตยทํ•จ์ •๋งŒ ๋‹ด๋Š”๋‹ค. ์ฝ”๋“œ์—์„œ ํŒŒ์ƒ ๊ฐ€๋Šฅํ•œ ์ •๋ณด๋Š” ์ ์ง€ ์•Š๋Š”๋‹ค.

โš ๏ธ ์ตœ์šฐ์„  ๊ทœ์น™

1. ์ฝ”๋“œ ์ž‘์—… ์ „ tdd-clean-arch ์Šคํ‚ฌ ๋กœ๋“œ

์ด ๋ ˆํฌ์˜ .claude/skills/tdd-clean-arch/์— ํฌํ•จ๋˜์–ด ์žˆ๋‹ค. ์ฝ”๋“œ ๋ณ€๊ฒฝ ๋„๊ตฌ(Edit/Write)๋ฅผ ์“ฐ๊ธฐ ์ „์— Skill ๋„๊ตฌ๋กœ ๋จผ์ € ํ˜ธ์ถœํ•œ๋‹ค.

  • ๋Œ€์ƒ ์ž‘์—… (์˜ˆ์™ธ ์—†์Œ): ์ปดํฌ๋„ŒํŠธยทํ›…ยท์œ ํ‹ธยทํŽ˜์ด์ง€ยท์Šคํ† ์–ด์˜ ์ƒ์„ฑ/์ˆ˜์ •/๋ฆฌํŒฉํ† ๋ง, API ํ•จ์ˆ˜(apis/)ยทQuery ํ›…(hooks/)ยทZustand ์Šคํ† ์–ด(store/) ์ž‘์—…, ํผยท๋ฐธ๋ฆฌ๋ฐ์ด์…˜ยท๋ผ์šฐํŒ…ยท๋ ˆ์ด์•„์›ƒ ๋ณ€๊ฒฝ. ์‚ฌ์šฉ์ž๊ฐ€ ์Šคํ‚ฌ์„ ์–ธ๊ธ‰ํ•˜์ง€ ์•Š์•„๋„ ํ•ด๋‹น๋˜๋ฉด ์ž๋™ ๋กœ๋“œ.
  • ๋‹จ์ˆœ ์งˆ๋ฌธ ๋‹ต๋ณ€ยทํŒŒ์ผ ํƒ์ƒ‰ยท์„ค๋ช…๋งŒ ํ•˜๋Š” ๊ฒฝ์šฐ๋Š” ์Šคํ‚ฌ ์—†์ด ์ง„ํ–‰ ๊ฐ€๋Šฅ.
  • ์Šคํ‚ฌ ์‚ฌ์šฉ ๋ถˆ๊ฐ€ ํ™˜๊ฒฝ์ด๋ฉด ์‚ฌ์šฉ์ž์—๊ฒŒ ์•Œ๋ฆฌ๊ณ  ์ง„ํ–‰ ์—ฌ๋ถ€ ํ™•์ธ.
  • ํ•ต์‹ฌ ์›์น™ (์ƒ์„ธ๋Š” ์Šคํ‚ฌ ๋‚ด๋ถ€): Karpathy ํ–‰๋™ ์›์น™(์ž‘๊ฒŒ ์‹œ์ž‘, ์š”์ฒญ ๋ฒ”์œ„๋งŒ, ๊ธฐ์กด ์Šคํƒ€์ผ ์œ ์ง€, ๋ถˆํ•„์š”ํ•œ ์ถ”์ƒํ™” ๊ธˆ์ง€) ยท feature ํด๋” โ†’ Domain/Application/Infrastructure/Presentation ๋ ˆ์ด์–ด ๋งคํ•‘ ยท TDD๋Š” vitest๊ฐ€ ์„ค์น˜๋œ ๊ฒฝ์šฐ์—๋งŒ ์ ์šฉ(์Šคํ‚ฌ์ด ์ž๋™ ํŒ์ • โ€” vitest ์„ค์น˜๋˜์–ด ์žˆ์Œ. ํ…Œ์ŠคํŠธ๋Š” ๋Œ€์ƒ ํŒŒ์ผ ์˜†์— co-locate).

2. ๋””์ž์ธ์€ ๋””์ž์ด๋„ˆ์˜ ๋ชซ โ€” ๋งŒ๋“ค์ง€๋„ ๋ฐ”๊พธ์ง€๋„ ์•Š๋Š”๋‹ค

  • ์›๋ž˜ ์—†๋˜ ๋ฌธ๊ตฌ(์นดํ”ผ)๋ฅผ ์ถ”๊ฐ€ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ยท์•ˆ๋‚ด ๋ฌธ๊ตฌยท๋ผ๋ฒจ์„ ์ง€์–ด๋‚ด์ง€ ๋ง ๊ฒƒ.
  • Figma๊ฐ€ ์ฃผ์–ด์ง€๊ฑฐ๋‚˜ ์‚ฌ์šฉ์ž๊ฐ€ ์ง์ ‘ ์š”๊ตฌํ•œ ๊ฒฝ์šฐ๊ฐ€ ์•„๋‹ˆ๋ฉด ๊ธฐ์กด ๋ ˆ์ด์•„์›ƒยท์Šคํƒ€์ผ์„ ๊ทธ๋Œ€๋กœ ์œ ์ง€ํ•œ๋‹ค.
  • ๋ชจ์…˜ ์ž‘์—… ์‹œ ์ •์  ์™ธํ˜•(๋ชจ์–‘ยท์ƒ‰ยทํฌ๊ธฐยท๋ฌธ๊ตฌ)์€ ๊ฑด๋“œ๋ฆฌ์ง€ ๋ง๊ณ  ๋ชจ์…˜๋งŒ ์ถ”๊ฐ€ํ•œ๋‹ค.
  • ์Šค์ผˆ๋ ˆํ†ค ๋“ฑ ์ƒˆ ์‹œ๊ฐ ์š”์†Œ๊ฐ€ ๋ถˆ๊ฐ€ํ”ผํ•˜๋ฉด ์ž„์˜๋กœ ๋””์ž์ธํ•˜์ง€ ๋ง๊ณ  ๋จผ์ € ์‚ฌ์šฉ์ž/๋””์ž์ด๋„ˆ์—๊ฒŒ ํ™•์ธํ•œ๋‹ค.

์ž‘์—… ๋ฐฉ์‹ ยท Git

  • Claude๋Š” git add/commit/push๋ฅผ ์ง์ ‘ ํ•˜์ง€ ์•Š๋Š”๋‹ค. ๊ตฌํ˜„ + ๊ฒ€์ฆ(pnpm lint, pnpm build)๊นŒ์ง€ ํ•˜๊ณ  ๋ฉˆ์ถ˜๋‹ค. ์ปค๋ฐ‹์€ ๊ฐœ๋ฐœ์ž๊ฐ€ ์ง์ ‘ ํ•œ๋‹ค.
  • ์ปค๋ฐ‹์€ ๋…ผ๋ฆฌ/ํ™”๋ฉด ๋‹จ์œ„๋กœ ์ž‘๊ฒŒ ์ชผ๊ฐ ๋‹ค โ€” ์„œ๋กœ ๋‹ค๋ฅธ ํ™”๋ฉดยท๊ธฐ๋Šฅ์„ ํ•œ ์ปค๋ฐ‹์— ๋ฌถ์ง€ ์•Š๋Š”๋‹ค. ์ปค๋ฐ‹ ๋ฉ”์‹œ์ง€๋ฅผ ์ œ์•ˆํ•  ๋•Œ๋„ ์ด ๋‹จ์œ„ ๊ธฐ์ค€.
  • PR ์ „ ๋ฆฌ๋ทฐ์—์„œ ๋ฐœ๊ฒฌํ•œ ๋ฌธ์ œ๋Š” ์‹ค์ œ๋กœ ์žฌํ˜„ยท๊ฒ€์ฆ๋œ ๊ฒƒ๋งŒ ์ˆ˜์ •ํ•œ๋‹ค. ์ถ”์ธก์„ฑ ๋ฐœ๊ฒฌ์œผ๋กœ ์ฝ”๋“œ๋ฅผ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค.

Commands

ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ €๋Š” pnpm ๊ณ ์ • (npm/yarn ๊ธˆ์ง€). pre-commit ํ›…(Husky + lint-staged)์ด eslint --fix + prettier๋ฅผ ์ž๋™ ์‹คํ–‰ํ•œ๋‹ค.

pnpm dev              # ๊ฐœ๋ฐœ ์„œ๋ฒ„
pnpm build            # tsc -b && vite build
pnpm build:dev        # --mode development ๋นŒ๋“œ (QA/dev ์„œ๋ฒ„์šฉ)
pnpm lint             # ESLint (--max-warnings 0)
pnpm lint:fix / format / format:check
pnpm test             # vitest watch (test:run = 1ํšŒ ์‹คํ–‰, test:coverage)

pnpm cap:sync         # build ํ›„ dist/๋ฅผ android+ios ๋„ค์ดํ‹ฐ๋ธŒ ํ”„๋กœ์ ํŠธ์— ๋ณต์‚ฌ
pnpm cap:sync:android / cap:sync:ios       # ๊ฐ :dev ๋ณ€ํ˜•์€ build:dev ๊ธฐ๋ฐ˜
pnpm cap:open:android / cap:open:ios   # Android Studio / Xcode ์—ด๊ธฐ
  • ์›น ์ฝ”๋“œ ์ˆ˜์ •์€ cap:sync๋ฅผ ํ•ด์•ผ ๋„ค์ดํ‹ฐ๋ธŒ ์•ฑ์— ๋ฐ˜์˜๋œ๋‹ค (webDir: dist ๋ณต์‚ฌ ๋ฐฉ์‹).
  • pnpm์€ node-linker=hoisted ๊ณ ์ • (.npmrc) โ€” isolated ๋ ˆ์ด์•„์›ƒ์œผ๋กœ ์„ค์น˜๋œ ์ƒํƒœ์—์„œ cap sync๋ฅผ ๋Œ๋ฆฌ๋ฉด iOS Podfile.lock์— .pnpm ํ•ด์‹œ ๊ฒฝ๋กœ๊ฐ€ ๋ฐ•ํ˜€ ๋‹ค๋ฅธ ๋จธ์‹ ์—์„œ ๊นจ์ง„๋‹ค(#321). Podfile.lock์˜ ๊ฒฝ๋กœ๋Š” ํ•ญ์ƒ ../../node_modules/{pkg} ํ˜•ํƒœ์—ฌ์•ผ ์ •์ƒ.

Tech Stack

React 19 ยท Vite 7 ยท TypeScript strict ยท Tailwind CSS 4 ยท React Router 7 ยท TanStack Query 5 ยท Zustand 5 ยท Axios ยท Capacitor 8 ยท PortOne V2 browser SDK ยท vite-plugin-svgr

๋””๋ ‰ํ† ๋ฆฌ ๊ตฌ์กฐ

src/ ์•„๋ž˜ feature-based ํด๋”๋ง. ํ•œ feature๊ฐ€ ๋ชจ๋“  ํด๋”๋ฅผ ๊ฐ–์ง€๋Š” ์•Š๋Š”๋‹ค (์˜ˆ: member/file/my๋Š” apis+hooks๋งŒ ์กด์žฌ).

  • apis/{feature}/ โ€” Axios ํ˜ธ์ถœ ํ•จ์ˆ˜ (*.api.ts)
  • hooks/{feature}/ โ€” Query/Mutation ํ›…. hooks/common/ โ€” ๊ณต์šฉ ํ›… (useInfiniteScroll, useDebounceValue, useGeolocation, useCarousel ๋“ฑ โ€” ์ƒˆ๋กœ ๋งŒ๋“ค๊ธฐ ์ „ ํ™•์ธ)
  • components/{feature}/, components/common/ โ€” ๊ณต์šฉ ์ปดํฌ๋„ŒํŠธ๋Š” barrel(index.ts) ๋จผ์ € ํ™•์ธ (Header, BottomSheet, DialogBox, CTA_Button, TabBar, ToastMessage, EmptyView, chips ๋“ฑ)
  • pages/{feature}/, types/{feature}/, constants/{feature}/
  • store/ โ€” Zustand ์Šคํ† ์–ด (use{Name}.store.ts)
  • utils/ โ€” ๊ณต์šฉ ํ—ฌํผ (platform.ts, tokenStorage.ts, dateFormat.ts, timeAgo.ts ๋“ฑ)
  • lib/ โ€” axiosInstance, setUpInterceptors(ํ† ํฐ ๋ฆฌํ”„๋ ˆ์‹œ), billing/(๋„ค์ดํ‹ฐ๋ธŒ IAP ๋ธŒ๋ฆฟ์ง€), payment/(PortOne)
  • layouts/ โ€” ์•„๋ž˜ Layout ์„น์…˜ ์ฐธ๊ณ 
  • router/Router.tsx โ€” ๋ชจ๋“  ๋ผ์šฐํŠธ๊ฐ€ ์ด ํ•œ ํŒŒ์ผ์— ์žˆ์Œ
  • android/, ios/ โ€” Capacitor ๋„ค์ดํ‹ฐ๋ธŒ ํ”„๋กœ์ ํŠธ

feature ๋„๋ฉ”์ธ: auth, mainPage, photoLab(ํ˜„์ƒ์†Œ), photoFeed(์‚ฌ์ง„์ˆ˜๋‹ค), photoManage(ํ˜„์ƒ๊ด€๋ฆฌ/์ธํ™”), photoRestoration, mypage, credit, payment, developmentHistory, filmCameraGuide, member, file, my. (pages/demoDay๋Š” ์ž„์‹œ ์ด๋ฒคํŠธ ํŽ˜์ด์ง€)

Naming

๋Œ€์ƒ ๊ทœ์น™
์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ PascalCase (Header.tsx)
ํ›… ํŒŒ์ผ camelCase (useRecentSearches.ts)
API ํŒŒ์ผ {name}.api.ts
์Šคํ† ์–ด ํŒŒ์ผ use{Name}.store.ts
SVG ํŒŒ์ผ kebab-case (arrow-left.svg)
SVG export PascalCase + Icon ์ ‘๋ฏธ์‚ฌ (ArrowLeftIcon)
ํƒ€์ž…/์ธํ„ฐํŽ˜์ด์Šค PascalCase
์ƒ์ˆ˜ UPPER_SNAKE_CASE

Styling

  • ํฌ๊ธฐ ๋‹จ์œ„๋Š” rem (px / 16 = rem). ๋””์ž์ธ ์‹œ์•ˆ์˜ px ๊ฐ’์„ ๊ทธ๋Œ€๋กœ ์ ์ง€ ๋ง ๊ฒƒ.
  • Path Alias @/ โ†’ src/.
  • ์ปค์Šคํ…€ ์ปฌ๋Ÿฌ๋Š” src/index.css์˜ @theme ๋ธ”๋ก์—๋งŒ ์ •์˜: orange-100~900 + orange-450, neutral-0~1000 + neutral-{750,850,875}. ์ž„์˜ hex ์ง์ ‘ ์‚ฌ์šฉ ๊ธˆ์ง€.
  • ๊ธ€๋กœ๋ฒŒ ๊ฐ’์€ :root CSS ๋ณ€์ˆ˜: --tabbar-height, --fab-gap.
  • prettier-plugin-tailwindcss๊ฐ€ ํด๋ž˜์Šค ์ˆœ์„œ ์ž๋™ ์ •๋ ฌ.
  • ํฐํŠธ: ๊ธฐ๋ณธ Pretendard Variable, ํฌ์ธํŠธ์šฉ .font-ydestreet.

์Šคํฌ๋กค ๊ตฌ์กฐ โ€” ํ•จ์ • ์ฃผ์˜

  • ์‹ค์ œ ์Šคํฌ๋กค๋Ÿฌ๋Š” ํ•ญ์ƒ #root๋‹ค. RootLayout์ด min-h-dvh(๋ถˆํ™•์ • ๋†’์ด)๋ผ ํŽ˜์ด์ง€์˜ h-full์ด ํ•ด์„๋˜์ง€ ์•Š๋Š”๋‹ค โ†’ ํŽ˜์ด์ง€ ์•ˆ์— flex-1 overflow-y-auto ๋‚ด๋ถ€ ์Šคํฌ๋กค ์ปจํ…Œ์ด๋„ˆ๋ฅผ ๋งŒ๋“ค์–ด๋„ ์˜ค๋ฒ„ํ”Œ๋กœ๊ฐ€ ์ƒ๊ธฐ์ง€ ์•Š๋Š” ์ฃฝ์€ ์ปจํ…Œ์ด๋„ˆ๊ฐ€ ๋œ๋‹ค. ์ง„์งœ ๋‚ด๋ถ€ ์Šคํฌ๋กค์€ ๋ ˆ์ด์•„์›ƒ ๋†’์ด ์ฒด์ธ ๊ฐœํŽธ์ด ํ•„์š”ํ•œ ๋ณ„๋„ ์ž‘์—….
  • 100dvh ๋Œ€์‹  100% ์‚ฌ์šฉ (PWA โ€” ์ฃผ์†Œ์ฐฝ ์ถค ๋ฐฉ์ง€, body ์Šคํฌ๋กค ์•ˆ ์”€).
  • ๊ณ ์ • ํ—ค๋”๋Š” #root ์Šคํฌ๋กค ๊ธฐ์ค€ sticky ํŒจํ„ด: <div className="sticky top-0 z-20 -mx-4 bg-neutral-900 px-4"> (-mx-4/px-4๋Š” RootLayout์˜ px-4 ํŒจ๋”ฉ ์œ„๋กœ ๋ฐฐ๊ฒฝ ํ™•์žฅ. PhotoLabDetailPage ์ฐธ๊ณ ).
  • overscroll-behavior: none์€ body์— ์ด๋ฏธ ์žˆ๋‹ค. ๊ฐœ๋ณ„ ์š”์†Œ์— overscroll-y-none์„ ์ถ”๊ฐ€ํ•˜์ง€ ๋ง ๊ฒƒ โ€” ์˜ค๋ฒ„ํ”Œ๋กœ ์—†๋Š” ์ปจํ…Œ์ด๋„ˆ์— ๋ถ™์œผ๋ฉด ํœ  ์Šคํฌ๋กค์ด ์™„์ „ํžˆ ์ฃฝ๋Š”๋‹ค.
  • (Claude ๊ฒ€์ฆ ํŒ) ๋ธŒ๋ผ์šฐ์ € ์ž๋™ํ™”(CDP)์˜ ํ•ฉ์„ฑ ์Šคํฌ๋กค์€ ๊ฐ€๋กœ ์บ๋Ÿฌ์…€(snap-x) ์œ„์—์„œ ์ด๋ฒคํŠธ๊ฐ€ ์†Œ๋ฉธํ•œ๋‹ค โ€” ์บ๋Ÿฌ์…€ ๊ด€๋ จ ์Šคํฌ๋กค ๋™์ž‘์€ CDP๋กœ ํŒ์ •ํ•˜์ง€ ๋ง๊ณ  ์‚ฌ์šฉ์ž ์‹ค๊ธฐ๊ธฐ ํ™•์ธ์„ ์š”์ฒญํ•  ๊ฒƒ.

SVG ์•„์ด์ฝ˜

import { ArrowLeftIcon } from "@/assets/icon";
<ArrowLeftIcon className="h-6 w-6 text-orange-500" />;
  • SVGO๊ฐ€ fill/stroke๋ฅผ currentColor๋กœ ๋ณ€ํ™˜ โ†’ text-*๋กœ ์ƒ‰์ƒ ์ œ์–ด. viewBox ์œ ์ง€, width/height ์ œ๊ฑฐ โ†’ Tailwind ํด๋ž˜์Šค๋กœ ์‚ฌ์ด์ฆˆ ์ œ์–ด.
  • ์ถ”๊ฐ€ ์‹œ: src/assets/icon/์— kebab-case๋กœ ์ €์žฅ โ†’ index.ts์— export { default as XxxIcon } from "./xxx.svg?react"; ์ถ”๊ฐ€.
  • Figma export SVG๋Š” ์ˆ˜์ •ํ•˜์ง€ ๋ง๊ณ  ๊ทธ๋Œ€๋กœ ์ €์žฅ.

API ยท TanStack Query

์„œ๋ฒ„ ์‘๋‹ต์€ ApiResponse<T> ๋ž˜ํผ(success/message/data, @/types/common/apiResponse). API ํ•จ์ˆ˜์—์„œ !body.success๋ฉด throw:

// apis/{feature}/{name}.api.ts
export async function getResource(params: Params): Promise<ApiResponse<Data>> {
  const res = await axiosInstance.get<ApiResponse<Data>>("/endpoint", {
    params,
  });
  if (!res.data.success) throw new Error(res.data.message);
  return res.data;
}
  • ๋ฌดํ•œ์Šคํฌ๋กค: useInfiniteQuery + pagination.hasNext (hooks/photoLab/usePhotoLabList ์ฐธ๊ณ ).
  • queryKey ์ปจ๋ฒค์…˜: ["{domain}", "{action}", ...params]. ๊ธฐ์กด ํ‚ค ์ผ๋ถ€๋Š” ์ด ๊ทœ์น™์„ ์•ˆ ๋”ฐ๋ฅด๋Š”๋ฐ, ๋ฌด๊ด€ํ•œ ์ž‘์—…์—์„œ ๊ณ ์น˜๋ ค ๋“ค์ง€ ๋ง ๊ฒƒ.
  • ๋ฐฐ์—ด ์ฟผ๋ฆฌ ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ์ฝค๋งˆ join ํ›„ ์ „์†ก (apis/photoLab/photoLab.api.ts ์ฐธ๊ณ ).
  • ์„œ๋ฒ„ ID๋Š” TSID โ†’ JSON์—์„œ string์ด๋‹ค (photoLabId, regionId, productId ๋“ฑ). number๋กœ ํŒŒ์‹ฑยท์„ ์–ธํ•˜์ง€ ๋ง ๊ฒƒ. ์ผ๋ถ€ ๋ ˆ๊ฑฐ์‹œ ID๋Š” number๋กœ ๋‚จ์•„ ์žˆ์ง€๋งŒ ์ƒˆ ์ฝ”๋“œ๋Š” string.
  • ์ฆ๊ฒจ์ฐพ๊ธฐ๋ฅ˜ ๋‚™๊ด€์  ์—…๋ฐ์ดํŠธ ์ปจ๋ฒค์…˜: onMutate์—์„œ ๋ถˆ๋ฆฌ์–ธ(isFavorite)๋งŒ ํ”Œ๋ฆฝ, ์นด์šดํŠธ(favoriteCount)๋Š” ยฑ1 ์ˆ˜๋™ ๊ณ„์‚ฐํ•˜์ง€ ์•Š๋Š”๋‹ค โ€” onSettled์˜ invalidate๋กœ ์„œ๋ฒ„ ์ •์‚ฐ (hooks/photoLab/useFavoriteToggle ์ฐธ๊ณ ).

์ธ์ฆ (Auth)

  • accessToken ์ €์žฅ์†Œ๋Š” ํ”Œ๋žซํผ๋ณ„๋กœ ๋‹ค๋ฅด๋‹ค: ์›น=localStorage, ๋„ค์ดํ‹ฐ๋ธŒ=SecureStoragePlugin. ๋ฐ˜๋“œ์‹œ utils/tokenStorage.ts๋ฅผ ๊ฒฝ์œ ํ•˜๊ณ  localStorage๋ฅผ ์ง์ ‘ ๋งŒ์ง€์ง€ ๋ง ๊ฒƒ.
  • refreshToken์€ httpOnly ์ฟ ํ‚ค (ํด๋ผ์ด์–ธํŠธ ์ ‘๊ทผ ๋ถˆ๊ฐ€). 401 โ†’ /auth/reissue โ†’ ์›์š”์ฒญ ์žฌ์‹œ๋„ + ์ค‘๋ณต refresh ๋ฐฉ์ง€ ํ๊ฐ€ lib/setUpInterceptors.ts์— ๊ตฌํ˜„๋˜์–ด ์žˆ๋‹ค. ์ธ์ฆ ๊ด€๋ จ ์ˆ˜์ • ์ „์— ์ด ํŒŒ์ผ์„ ๋จผ์ € ์ฝ์„ ๊ฒƒ.
  • signupToken: ๊ฐ€์ž… ๋ฏธ์™„๋ฃŒ ์‚ฌ์šฉ์ž์šฉ ์ž„์‹œ ํ† ํฐ. accessToken ์—†์„ ๋•Œ ๋Œ€์‹  ์ฒจ๋ถ€๋˜๋ฉฐ, 401์ด์–ด๋„ refresh ๋Œ€์ƒ์ด ์•„๋‹ˆ๋‹ค.
  • ์†Œ์…œ ๋กœ๊ทธ์ธ: ์นด์นด์˜ค(์›น=redirect ๋ฐฉ์‹, ๋„ค์ดํ‹ฐ๋ธŒ=capacitor3-kakao-login ํ”Œ๋Ÿฌ๊ทธ์ธ), ์• ํ”Œ(@capacitor-community/apple-sign-in). ๊ด€๋ จ ์œ ํ‹ธ์€ utils/auth/.

ํ”Œ๋žซํผ ๋ถ„๊ธฐ ยท ๊ฒฐ์ œ

  • ํ”Œ๋žซํผ ํŒ๋ณ„์€ utils/platform.ts ํ—ฌํผ ์‚ฌ์šฉ (isAndroidApp/isIosApp/getPaymentProvider), utils/auth/envUtils.ts์˜ isNativeApp.
  • ๊ฒฐ์ œ๋Š” ํ•˜๋‚˜์˜ PaymentPage์—์„œ 3๋ถ„๊ธฐ: android=GOOGLE_PLAY(์ธ์•ฑ๊ฒฐ์ œ), ios=APPLE_IAP(StoreKit2), web=PORTONE(์›น๊ฒฐ์ œ). ํฌ๋ ˆ๋”ง ์ƒํ’ˆ ๋ชฉ๋ก์€ provider ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์„œ๋ฒ„ ์กฐํšŒ.
  • ๋„ค์ดํ‹ฐ๋ธŒ IAP: ์ปค์Šคํ…€ ํ”Œ๋Ÿฌ๊ทธ์ธ FindersBillingPlugin(android/ios ๋„ค์ดํ‹ฐ๋ธŒ ์ฝ”๋“œ) โ†” lib/billing/finders-billing.ts ๋ธŒ๋ฆฟ์ง€. ์˜์ˆ˜์ฆ ๊ฒ€์ฆ์€ ์„œ๋ฒ„(/payments/{google|apple}/verify). Google์€ ์„œ๋ฒ„๊ฐ€ consume, Apple์€ ํด๋ผ์ด์–ธํŠธ๊ฐ€ finishPurchase โ€” ์ด ๊ณ„์•ฝ์„ ๋ฐ”๊พธ๋ฉด ๊ฒฐ์ œ๊ฐ€ ์œ ์‹ค๋œ๋‹ค.
  • PortOne: lib/payment/portone.ts. paymentId๋Š” ๋ฐฑ์—”๋“œ๊ฐ€ ์ƒ์„ฑ(์‚ฌ์ „๋“ฑ๋ก โ†’ requestPayment โ†’ complete). ๋ชจ๋ฐ”์ผ ์›น redirect ๋ณต๊ท€ ์‹œ sessionStorage๋กœ ๋ณต์›(pendingPortonePayment.ts).
  • ์ƒํ’ˆ ID ๋ถˆ๋ณ€์‹: DB์˜ externalProductId === ์Šคํ† ์–ด ์ฝ˜์†”(Play Console/App Store Connect) product ID === ํด๋ผ๊ฐ€ ์กฐํšŒํ•˜๋Š” SKU. ํ•˜๋‚˜๋ผ๋„ ์–ด๊ธ‹๋‚˜๋ฉด ํ•ด๋‹น ์Šคํ† ์–ด ๊ฒฐ์ œ๊ฐ€ ์ „๊ฑด ์‹คํŒจํ•œ๋‹ค.
  • verify์˜ PAYMENT_410(์ค‘๋ณต ๊ฒฐ์ œ)์€ ์„ฑ๊ณต์œผ๋กœ ์ทจ๊ธ‰ํ•œ๋‹ค โ€” ์ด๋ฏธ ์ง€๊ธ‰๋œ ๊ตฌ๋งค์˜ ์žฌ์ „์†ก ์ผ€์ด์Šค์ด๋ฏ€๋กœ ์ •์ƒ ํ”Œ๋กœ์šฐ(finish ๋“ฑ)๋ฅผ ๊ณ„์† ์ง„ํ–‰.
  • ๋ฏธ๋ฐ˜์˜ ๊ตฌ๋งค ์ž๊ฐ€๋ณต๊ตฌ: useReconcileGooglePurchases/useReconcileApplePurchases๊ฐ€ ๋ฏธ์™„๋ฃŒ ๊ตฌ๋งค๋ฅผ ์žฌ๊ฒ€์ฆํ•œ๋‹ค. ๊ฒฐ์ œ ์ฝ”๋“œ ์ˆ˜์ • ์‹œ ์ด ๊ฒฝ๋กœ๋ฅผ ๊นจ๋œจ๋ฆฌ์ง€ ๋ง ๊ฒƒ.
  • iOS ์•ฑ์—์„œ PortOne(์›น๊ฒฐ์ œ) UI๊ฐ€ ๋…ธ์ถœ๋˜๋ฉด ์•ฑ์Šคํ† ์–ด ๋ฆฌ์  ์‚ฌ์œ (๊ฐ€์ด๋“œ๋ผ์ธ 3.1.1) โ€” PaymentPage ํ”Œ๋žซํผ ๋ถ„๊ธฐ ์ˆ˜์ • ์‹œ ์ฃผ์˜.
  • ๊ฒฐ์ œ E2E๋Š” ๋กœ์ปฌ ๋ถˆ๊ฐ€: Google=๋‚ด๋ถ€ํ…Œ์ŠคํŠธ ํŠธ๋ž™ ์„œ๋ช…๋นŒ๋“œ ํ•„์š”, Apple=์‹ค๊ธฐ๊ธฐ sandbox/TestFlight (Xcode ๋กœ์ปฌ .storekit ํŠธ๋žœ์žญ์…˜์€ ์„œ๋ฒ„ ๊ฒ€์ฆ์ด ์•ˆ ๋จ).

State ๋ถ„๋ฆฌ

  • ์„œ๋ฒ„ ์ƒํƒœ๋Š” TanStack Query.
  • ํด๋ผ์ด์–ธํŠธ ์ „์—ญ ์ƒํƒœ๋งŒ Zustand: useAuth, useLoginModal, useNewPostState, usePhotoLabFilter, usePrintOrder, useAddressId, usePaymentOrder.
  • ํŽ˜์ด์ง€ ๋กœ์ปฌ ์ƒํƒœ๋Š” useState/useReducer. ๊ตณ์ด store๋กœ ๋Œ์–ด์˜ฌ๋ฆฌ์ง€ ๋ง ๊ฒƒ.

Layouts

  • ๊ธฐ๋ณธ ํŽ˜์ด์ง€ โ†’ RootLayout (safe-area, max-width 480px ๋ชจ๋ฐ”์ผ ์ปจํ…Œ์ด๋„ˆ).
  • 5ํƒญ ๋ฉ”์ธ ๋„ค๋น„ ํŽ˜์ด์ง€ โ†’ FooterLayout (TabBar. Router์—์„œ <Route element={<FooterLayout />}> ๊ทธ๋ฃน).
  • ๋งˆ์ดํŽ˜์ด์ง€ ํ”Œ๋กœ์šฐ โ†’ MyPageLayout, ์ธํ™” ํ”Œ๋กœ์šฐ โ†’ PhotoManageLayout.
  • MyPage/PhotoManage ํ•˜์œ„ ๋ผ์šฐํŠธ๋Š” route handle({ title, isTab, showBack, hideHeader })๋กœ ํ—ค๋”๋ฅผ ์ œ์–ดํ•œ๋‹ค โ€” ์ƒˆ ํ•˜์œ„ ํŽ˜์ด์ง€ ์ถ”๊ฐ€ ์‹œ Router.tsx์˜ ๊ธฐ์กด handle ํŒจํ„ด์„ ๋”ฐ๋ฅผ ๊ฒƒ.

Display Formatting

Field Format
distanceKm {value}km
avgWorkTimeMinutes {value}๋ถ„
workCount {value}๊ฑด

ํ™˜๊ฒฝ ๋ณ€์ˆ˜ (.env, ์ „๋ถ€ VITE_PUBLIC_ ์ ‘๋‘์‚ฌ)

API_URL ยท KAKAO_REDIRECT_URI ยท KAKAO_JS_KEY ยท KAKAO_REST_API_KEY ยท KAKAO_NATIVE_APP_KEY ยท APPLE_CLIENT_ID ยท APPLE_REDIRECT_URI ยท PORTONE_STORE_ID ยท PORTONE_CHANNEL_KEY

๋„ค์ดํ‹ฐ๋ธŒ(Capacitor) ์ฃผ์˜์‚ฌํ•ญ

  • ID ์ฒด๊ณ„๊ฐ€ ํ—ท๊ฐˆ๋ฆฐ๋‹ค: Capacitor appIdยทiOS ๋ฒˆ๋“คIDยทAndroid namespace๋Š” com.finders.app, Android ์Šคํ† ์–ด์šฉ applicationId๋งŒ com.finders (build.gradle์—์„œ ์˜๋„์ ์œผ๋กœ ๋ถ„๋ฆฌ โ€” com.finders.app์€ Play์—์„œ ํƒ€ ๊ณ„์ •์ด ์ ์œ ).
  • ์•ฑ WebView์˜ ์ถœ์ฒ˜(origin)๋Š” ์›น๊ณผ ๋‹ค๋ฅด๋‹ค: Android=https://localhost, iOS=capacitor://localhost(๊ณ ์ • โ€” https๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์—†์Œ). ๋ฐฑ์—”๋“œ CORS๊ฐ€ ์ด ๋‘ ์ถœ์ฒ˜๋ฅผ allowCredentials(true)์™€ ํ•จ๊ป˜ ํ—ˆ์šฉํ•ด์•ผ ํ•œ๋‹ค. "์•ฑ์—์„œ๋งŒ API๊ฐ€ 403 CORS"๋ฉด ์‹ญ์ค‘ํŒ”๊ตฌ ์ด ๋ฌธ์ œ.
  • ๊ฐ™์€ ์ด์œ ๋กœ ์™ธ๋ถ€ ๋ฆฌ์†Œ์Šค(์นด์นด์˜ค ์ง€๋„ SDK ๋“ฑ)๋Š” ์ƒ๋Œ€ URL(//...)์ด ๊นจ์ง„๋‹ค โ€” ์ ˆ๋Œ€ URL(https://) ๋ช…์‹œ.
  • ์œ„์น˜์ •๋ณด ๋“ฑ ๊ธฐ๊ธฐ ๊ถŒํ•œ์€ ์›น ์ฝ”๋“œ๋งŒ์œผ๋กœ ์•ˆ ๋˜๊ณ  android/ยทios/์˜ ๋งค๋‹ˆํŽ˜์ŠคํŠธ/plist ๊ถŒํ•œ ์„ ์–ธ์ด ํ•„์š”ํ•˜๋‹ค.
  • ์นด์นด์˜ค OAuth ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ(kakao{key}://oauth)๋Š” AuthCodeHandlerActivity์˜ intent-filter๊ฐ€ ๋ฐ›๋Š”๋‹ค (AndroidManifest์— ๋ฐ˜์˜๋จ โ€” MainActivity๋กœ ์˜ฎ๊ธฐ๋ฉด ๋„ค์ดํ‹ฐ๋ธŒ ์นด์นด์˜ค ๋กœ๊ทธ์ธ์ด ๊นจ์ง„๋‹ค).
  • release ๋นŒ๋“œ๋Š” JS console.log๊ฐ€ logcat์— ์•ˆ ์ฐํžŒ๋‹ค (๋„ค์ดํ‹ฐ๋ธŒ Log.*๋งŒ ๋‚˜์˜ด). JS ๋ ˆ๋ฒจ ๋””๋ฒ„๊น…์€ debug ๋นŒ๋“œ๋กœ.
  • Android ์„œ๋ช… release ๋นŒ๋“œ: pnpm build โ†’ npx cap sync android โ†’ android/์—์„œ gradlew assembleRelease (Play Store์šฉ AAB๋Š” bundleRelease). Capacitor 8์€ JDK 21 ํ•„์š” โ€” invalid source release: 21 ์—๋Ÿฌ๋Š” ๋กœ์ปฌ์— ๋‚ฎ์€ JDK(JAVA_HOME ๋˜๋Š” ์‚ฌ์šฉ์ž ์ „์—ญ gradle.properties)๊ฐ€ ์žกํžŒ ๊ฒƒ์ด๋‹ˆ -Dorg.gradle.java.home=<JDK21 ๊ฒฝ๋กœ>๋กœ ์˜ค๋ฒ„๋ผ์ด๋“œ. ์„œ๋ช… ๋น„๋ฐ€์ •๋ณด๋Š” android/keystore.properties(gitignore) โ€” ์ ˆ๋Œ€ ์ปค๋ฐ‹ ๊ธˆ์ง€.
  • User-Agent์— FindersApp์ด append๋˜์–ด ์žˆ์Œ (์„œ๋ฒ„/์™ธ๋ถ€ ์„œ๋น„์Šค์—์„œ ์•ฑ ํŒ๋ณ„์šฉ).