diff --git a/.claude/rules/data-fetching.md b/.claude/rules/data-fetching.md index 661ce882..f22192bc 100644 --- a/.claude/rules/data-fetching.md +++ b/.claude/rules/data-fetching.md @@ -54,3 +54,55 @@ export const postApi = { ['posts', id] // single item ['attendance', { generationNumber }] // with filter ``` + +### React Query — Suspense Pattern (preferred) + +When the same query's loading/error handling is duplicated across 2+ components, use `useSuspenseQuery` + Next.js `loading.tsx` / `error.tsx` instead of manual `isLoading` / `isError` checks. + +```ts +// hooks/queries/mypage/useMyMemberQuery.ts +import { useSuspenseQuery, skipToken } from '@tanstack/react-query'; + +export function useMyMemberQuery() { + const clubId = useClubId(); + return useSuspenseQuery({ + queryKey: ['mypage', 'me', clubId], + queryFn: clubId + ? () => mypageApi.getMe(clubId).then((res) => res.data.data) + : skipToken, + }); +} +``` + +- `useSuspenseQuery`: `data` is always defined (no `isLoading` / `isError` / `!data` guards needed) +- `skipToken`: replaces `enabled` option (not supported by `useSuspenseQuery`) +- `loading.tsx`: placed in the route segment, handles Suspense fallback +- `error.tsx`: placed in the route segment, handles ErrorBoundary with `reset` prop + +```tsx +// app/(private)/(main)/mypage/loading.tsx +export default function Loading() { + return

로딩 중...

; +} + +// app/(private)/(main)/mypage/error.tsx +'use client'; +export default function Error({ reset }: { error: Error; reset: () => void }) { + return ; +} +``` + +**When to use:** Client components that fetch user-scoped data shown on page load (e.g., my profile, my clubs). +**When NOT to use:** Queries triggered by user interaction (e.g., search, infinite scroll) — use regular `useQuery` for those. + +### Import Rule — Barrel Export Caveat + +Client-side hooks must **NOT** import from `@/lib/apis` (barrel). The barrel re-exports `apiServer` which imports `next/headers`, causing build errors in client components. Always use direct imports: + +```ts +// Good +import { mypageApi } from '@/lib/apis/mypage'; + +// Bad — pulls in apiServer → next/headers +import { mypageApi } from '@/lib/apis'; +``` diff --git a/.claude/rules/design-tokens.md b/.claude/rules/design-tokens.md index ef715412..0751a6af 100644 --- a/.claude/rules/design-tokens.md +++ b/.claude/rules/design-tokens.md @@ -36,8 +36,9 @@ Composite classes defined with `@utility`. Includes font-size + line-height + fo | `typo-h1` | Heading 1 | 40px / bold | | `typo-h2` | Heading 2 | 32px / bold | | `typo-h3` | Heading 3 | 24px / semibold | -| `typo-sub1` | Subheading 1 | 18px / semibold | -| `typo-sub2` | Subheading 2 | 16px / semibold | +| `typo-sub1` | Subheading 1 | 16px / black | +| `typo-sub2` | Subheading 2 | 18px / semibold | +| `typo-sub3` | Subheading 3 | 16px / semibold | | `typo-body1` | Body text 1 | 16px / 470 | | `typo-body2` | Body text 2 | 14px / 450 | | `typo-caption1` | Caption 1 | 12px / semibold | diff --git a/.claude/settings.local.json b/.claude/settings.local.json index bb5a248c..e9fbf98d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -2,21 +2,13 @@ "permissions": { "allow": [ "Bash(gh pr:*)", + "Bash(gh run:*)", "Bash(git push:*)", + "Bash(git fetch:*)", "mcp__figma__get_design_context", - "Bash(grep -E \"\\\\.tsx$\")", - "Bash(find \"D:\\\\project\\\\weeth-client\\\\src\\\\app\\\\\\(private\\)\" -name \"layout.tsx\" -type f)", - "Bash(find \"D:\\\\project\\\\weeth-client\\\\src\\\\app\\\\\\(private\\)\\\\\\(main\\)\" -name \"page.tsx\" -type f)", - "Bash(grep -r PageNavigation D:projectweeth-clientsrc --include=*.tsx --include=*.ts)", - "Bash(ls -la \"D:\\\\project\\\\weeth-client\\\\src\\\\app\\\\\\(private\\)\\\\\\(intro\\)\\\\home\")", - "Bash(find \"D:\\\\project\\\\weeth-client\\\\src\\\\app\\\\\\(private\\)\" -type f -name \"layout.tsx\")", "mcp__figma__get_screenshot", - "Bash(find D:projectweeth-clientsrccomponentsmypage -type f -name *.tsx -o -name *.ts)", - "Bash(ls -la \"D:\\\\project\\\\weeth-client\\\\src\\\\app\\\\\\(private\\)\\\\\\(main\\)\\\\mypage\")", - "Bash(ls -la \"D:\\\\project\\\\weeth-client\\\\src\\\\app\\\\\\(private\\)\\\\\\(main\\)\\\\mypage\\\\edit\")", - "Bash(find D:projectweeth-clientsrccomponentsui -type f \\\\\\(-name *.tsx -o -name *.ts \\\\\\))", - "Bash(find D:/project/weeth-client/src/constants -name *.ts)", - "Bash(grep -r \"POSTHOG\\\\|PostHog\" D:projectweeth-client/.env*)" + "Bash(gh pr *)", + "Read(//c/Users/wlslw/.claude/projects/**)" ] } } diff --git a/.claude/skills/pr-writer/SKILL.md b/.claude/skills/pr-writer/SKILL.md index bb55ae5f..8a3ca4f5 100644 --- a/.claude/skills/pr-writer/SKILL.md +++ b/.claude/skills/pr-writer/SKILL.md @@ -3,6 +3,21 @@ name: pr-writer description: Analyzes code changes and generates a PR body following .github/pull_request_template.md format. Use for PR creation or PR update requests. --- +## Step 0. Pre-flight Checks (Required — run BEFORE PR creation) + +Run these four checks and verify each passes. If a check fails on newly changed files, fix and re-run; if it fails on pre-existing issues unrelated to this branch, note it in the PR body. + +```bash +pnpm typecheck # TypeScript +pnpm lint # ESLint (0 errors; warnings ok) +pnpm format:check # Prettier +pnpm build # Next.js build +``` + +If `pnpm typecheck` / `pnpm build` fail with missing-module errors, run `pnpm install` first and retry. If `pnpm format:check` fails on files touched in this branch, run `pnpm prettier --write ` on only those files before proceeding. + +Do not proceed to Step 1 until all four checks are green (or failures are confirmed pre-existing and unrelated). + ## Step 1. Analyze Changes ```bash BASE_BRANCH=$(git remote show origin | sed -n '/HEAD branch/s/.*: //p') diff --git a/CLAUDE.md b/CLAUDE.md index adb878fe..1fa1ed4e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,7 @@ No hardcoded values. Always use token classes first. Ask user before adding new | Text | `text-text-strong` `text-text-normal` `text-text-alternative` `text-text-disabled` `text-text-inverse` | | Background | `bg-container-neutral` `bg-container-neutral-interaction` | | Button | `bg-button-primary` `bg-button-neutral` | -| Typography | `typo-h1~h3` `typo-sub1~2` `typo-body1~2` `typo-caption1~2` `typo-button1~2` | +| Typography | `typo-h1~h3` `typo-sub1~3` `typo-body1~2` `typo-caption1~2` `typo-button1~2` | | Spacing | `p-100~500` `gap-100~400` | → Full token list: `.claude/rules/design-tokens.md` diff --git a/amplify.yml b/amplify.yml index 0292d6f8..6ddfd4e0 100644 --- a/amplify.yml +++ b/amplify.yml @@ -8,6 +8,11 @@ frontend: - pnpm install --frozen-lockfile build: commands: + - | + set -e + matched=$(env | grep -E '^(PREVIEW_ACCESS_TOKEN|CLUB_ID)=') || grep_status=$? + if [ "${grep_status:-0}" -gt 1 ]; then exit "$grep_status"; fi + if [ -n "$matched" ]; then printf '%s\n' "$matched" >> .env.production; fi - pnpm build artifacts: baseDirectory: .next diff --git a/next.config.ts b/next.config.ts index 314bae6e..ae376798 100644 --- a/next.config.ts +++ b/next.config.ts @@ -2,6 +2,14 @@ import type { NextConfig } from 'next'; const nextConfig: NextConfig = { reactCompiler: true, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'weeth-s3-dev.s3.ap-northeast-2.amazonaws.com', + }, + ], + }, }; export default nextConfig; diff --git a/package.json b/package.json index a2bf432a..c722bac3 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,9 @@ "generate:tests": "tsx scripts/generate-tests.ts" }, "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "@gsap/react": "^2.1.2", "@hookform/resolvers": "^5.2.2", "@next/third-parties": "^16.2.3", @@ -54,14 +57,17 @@ "axios": "1.13.6", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dompurify": "^3.4.1", "embla-carousel-react": "^8.6.0", "framer-motion": "^12.35.2", "gsap": "^3.14.2", + "hast-util-to-html": "^9.0.5", "lottie-react": "^2.4.1", "lowlight": "^3.3.0", "lucide-react": "^0.468.0", "next": "16.1.6", "posthog-js": "^1.364.4", + "qr-code-styling": "^1.9.2", "radix-ui": "^1.4.3", "react": "19.2.3", "react-dom": "19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 69b87522..6e5e6d68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,15 @@ importers: .: dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@19.2.3) '@gsap/react': specifier: ^2.1.2 version: 2.1.2(gsap@3.14.2)(react@19.2.3) @@ -119,6 +128,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + dompurify: + specifier: ^3.4.1 + version: 3.4.1 embla-carousel-react: specifier: ^8.6.0 version: 8.6.0(react@19.2.3) @@ -128,6 +140,9 @@ importers: gsap: specifier: ^3.14.2 version: 3.14.2 + hast-util-to-html: + specifier: ^9.0.5 + version: 9.0.5 lottie-react: specifier: ^2.4.1 version: 2.4.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -143,6 +158,9 @@ importers: posthog-js: specifier: ^1.364.4 version: 1.364.4 + qr-code-styling: + specifier: ^1.9.2 + version: 1.9.2 radix-ui: specifier: ^1.4.3 version: 1.4.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) @@ -469,6 +487,28 @@ packages: resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} engines: {node: '>=18'} + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -2277,6 +2317,9 @@ packages: '@types/markdown-it@14.1.2': resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} @@ -2686,6 +2729,9 @@ packages: caniuse-lite@1.0.30001770: resolution: {integrity: sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==} + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2694,6 +2740,12 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + ci-info@4.4.0: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} @@ -2733,6 +2785,9 @@ packages: resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} engines: {node: '>= 0.8'} + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2875,8 +2930,8 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dompurify@3.3.3: - resolution: {integrity: sha512-Oj6pzI2+RqBfFG+qOaOLbFXLQ90ARpcGG6UePL82bJLtdsa6CYJD7nmiU8MW9nQNOtCHV3lZ/Bzq1X0QYbBZCA==} + dompurify@3.4.1: + resolution: {integrity: sha512-JahakDAIg1gyOm7dlgWSDjV4n7Ip2PKR55NIT6jrMfIgLFgWo81vdr1/QGqWtFNRqXP9UV71oVePtjqS2ebnPw==} dotenv@17.3.1: resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} @@ -3343,6 +3398,12 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hermes-estree@0.25.1: resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} @@ -3360,6 +3421,9 @@ packages: html-escaper@2.0.2: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -3934,6 +3998,9 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + mdn-data@2.0.14: resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} @@ -3947,6 +4014,21 @@ packages: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + micromatch@4.0.8: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} @@ -4276,6 +4358,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + prosemirror-changeset@2.4.0: resolution: {integrity: sha512-LvqH2v7Q2SF6yxatuPP2e8vSUKS/L+xAU7dPDC4RMyHMhZoGDfBC74mYuyYF4gLqOEG758wajtyhNnsTkuhvng==} @@ -4352,6 +4437,13 @@ packages: pure-rand@7.0.1: resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==} + qr-code-styling@1.9.2: + resolution: {integrity: sha512-RgJaZJ1/RrXJ6N0j7a+pdw3zMBmzZU4VN2dtAZf8ZggCfRB5stEQ3IoDNGaNhYY3nnZKYlYSLl5YkfWN5dPutg==} + engines: {node: '>=18.18.0'} + + qrcode-generator@1.5.2: + resolution: {integrity: sha512-pItrW0Z9HnDBnFmgiNrY1uxRdri32Uh9EjNYLPVC2zZ3ZRIIEqBoDgm4DkvDwNNDHTK7FNkmr8zAa77BYc9xNw==} + query-selector-shadow-dom@1.0.1: resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} @@ -4608,6 +4700,9 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -4669,6 +4764,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} @@ -4785,6 +4883,9 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} @@ -4875,6 +4976,21 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -4922,6 +5038,12 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} @@ -5061,6 +5183,9 @@ packages: use-sync-external-store: optional: true + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + snapshots: '@adobe/css-tools@4.4.4': {} @@ -5296,6 +5421,31 @@ snapshots: '@csstools/css-tokenizer@3.0.4': {} + '@dnd-kit/accessibility@3.1.1(react@19.2.3)': + dependencies: + react: 19.2.3 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@dnd-kit/utilities': 3.2.2(react@19.2.3) + react: 19.2.3 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@19.2.3)': + dependencies: + react: 19.2.3 + tslib: 2.8.1 + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -7096,6 +7246,10 @@ snapshots: '@types/linkify-it': 5.0.0 '@types/mdurl': 2.0.0 + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + '@types/mdurl@2.0.0': {} '@types/node@20.19.33': @@ -7538,6 +7692,8 @@ snapshots: caniuse-lite@1.0.30001770: {} + ccount@2.0.1: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -7545,6 +7701,10 @@ snapshots: char-regex@1.0.2: {} + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + ci-info@4.4.0: {} cjs-module-lexer@2.2.0: {} @@ -7577,6 +7737,8 @@ snapshots: dependencies: delayed-stream: 1.0.0 + comma-separated-tokens@2.0.3: {} + concat-map@0.0.1: {} convert-source-map@2.0.0: {} @@ -7694,7 +7856,7 @@ snapshots: dom-accessibility-api@0.6.3: {} - dompurify@3.3.3: + dompurify@3.4.1: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -8321,6 +8483,24 @@ snapshots: dependencies: function-bind: 1.1.2 + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + hermes-estree@0.25.1: {} hermes-parser@0.25.1: @@ -8335,6 +8515,8 @@ snapshots: html-escaper@2.0.2: {} + html-void-elements@3.0.0: {} + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -9097,6 +9279,18 @@ snapshots: math-intrinsics@1.1.0: {} + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.0 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + mdn-data@2.0.14: {} mdurl@2.0.0: {} @@ -9105,6 +9299,23 @@ snapshots: merge2@1.4.1: {} + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-encode@2.0.1: {} + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@2.0.2: {} + micromatch@4.0.8: dependencies: braces: 3.0.3 @@ -9360,7 +9571,7 @@ snapshots: '@posthog/core': 1.24.4 '@posthog/types': 1.364.4 core-js: 3.49.0 - dompurify: 3.3.3 + dompurify: 3.4.1 fflate: 0.4.8 preact: 10.29.0 query-selector-shadow-dom: 1.0.1 @@ -9394,6 +9605,8 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + property-information@7.1.0: {} + prosemirror-changeset@2.4.0: dependencies: prosemirror-transform: 1.11.0 @@ -9520,6 +9733,12 @@ snapshots: pure-rand@7.0.1: {} + qr-code-styling@1.9.2: + dependencies: + qrcode-generator: 1.5.2 + + qrcode-generator@1.5.2: {} + query-selector-shadow-dom@1.0.1: {} queue-microtask@1.2.3: {} @@ -9868,6 +10087,8 @@ snapshots: source-map@0.6.1: {} + space-separated-tokens@2.0.2: {} + sprintf-js@1.0.3: {} stable-hash@0.0.5: {} @@ -9965,6 +10186,11 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 @@ -10057,6 +10283,8 @@ snapshots: dependencies: punycode: 2.3.1 + trim-lines@3.0.1: {} + ts-algebra@2.0.0: {} ts-api-utils@2.4.0(typescript@5.9.3): @@ -10166,6 +10394,29 @@ snapshots: undici-types@6.21.0: {} + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 @@ -10229,6 +10480,16 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + w3c-keyname@2.2.8: {} w3c-xmlserializer@5.0.0: @@ -10357,3 +10618,5 @@ snapshots: '@types/react': 19.2.14 react: 19.2.3 use-sync-external-store: 1.6.0(react@19.2.3) + + zwitch@2.0.4: {} diff --git a/src/app/(private)/(auth)/club/create/layout.tsx b/src/app/(private)/(auth)/club/create/layout.tsx index 0d706f48..1b0c2928 100644 --- a/src/app/(private)/(auth)/club/create/layout.tsx +++ b/src/app/(private)/(auth)/club/create/layout.tsx @@ -1,9 +1,6 @@ -import { PublicHeader } from '@/components/layout/header/PublicHeader'; - export default function CreateClubLayout({ children }: { children: React.ReactNode }) { return (
-
{children}
); diff --git a/src/app/(private)/(auth)/club/select/page.tsx b/src/app/(private)/(auth)/club/select/page.tsx new file mode 100644 index 00000000..7c8c4a01 --- /dev/null +++ b/src/app/(private)/(auth)/club/select/page.tsx @@ -0,0 +1,27 @@ +import { redirect } from 'next/navigation'; +import { ClubList, HubProfile } from '@/components/auth/hub'; +import { apiServer } from '@/lib/apis/server'; +import type { ApiResponse } from '@/types/common'; +import type { ClubDto } from '@/types/mypage'; + +export default async function ClubSelectPage() { + const res = await apiServer.get>('/clubs'); + const clubs = res?.data ?? []; + + if (clubs.length === 1) { + const club = clubs[0]; + redirect(`/${club.id}/home`); + } + + return ( +
+ +
+ +
+
+ ); +} diff --git a/src/app/(private)/(auth)/hub/page.tsx b/src/app/(private)/(auth)/hub/page.tsx index a8aaea43..bffdd033 100644 --- a/src/app/(private)/(auth)/hub/page.tsx +++ b/src/app/(private)/(auth)/hub/page.tsx @@ -1,6 +1,8 @@ import { unstable_rethrow } from 'next/navigation'; import { HubActionCard, HubProfile } from '@/components/auth/hub'; import { apiServer } from '@/lib/apis/server'; +import type { ApiResponse } from '@/types/common'; +import type { ClubDto } from '@/types/mypage'; type CardVariant = 'create' | 'join' | 'go'; @@ -8,7 +10,6 @@ interface MembershipStatusResponse { data: { hasActiveClub: boolean; hasWaitingClub: boolean; - activeClub: { id: string } | null; }; } @@ -21,6 +22,8 @@ export default async function HubPage({ let cardOrder: CardVariant[]; let goHref: string | undefined; + let goClubId: string | undefined; + let goClubName: string | undefined; const status = await apiServer .get('/clubs/membership-status') @@ -30,7 +33,23 @@ export default async function HubPage({ }); const hasActiveClub = status?.data?.hasActiveClub ?? false; - if (hasActiveClub) goHref = '/home'; + + if (hasActiveClub) { + const clubsRes = await apiServer.get>('/clubs').catch((err) => { + unstable_rethrow(err); + return null; + }); + const clubs = clubsRes?.data ?? []; + + if (clubs.length === 1) { + const club = clubs[0]; + goHref = `/${club.id}/home`; + goClubId = club.id; + goClubName = club.name; + } else { + goHref = '/club/select'; + } + } if (intent === 'create') { cardOrder = ['create', 'join', 'go']; @@ -56,6 +75,8 @@ export default async function HubPage({ variant={variant} href={hrefMap[variant]} isPrimary={index === 0} + clubId={variant === 'go' ? goClubId : undefined} + clubName={variant === 'go' ? goClubName : undefined} /> ))} diff --git a/src/app/(private)/(intro)/home/layout.tsx b/src/app/(private)/(intro)/home/layout.tsx deleted file mode 100644 index 53a785b9..00000000 --- a/src/app/(private)/(intro)/home/layout.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { ReactNode } from 'react'; - -export default function HomeLayout({ - children, -}: Readonly<{ - children: ReactNode; -}>) { - return
{children}
; -} diff --git a/src/app/(private)/(main)/attendance/history/page.tsx b/src/app/(private)/(main)/attendance/history/page.tsx deleted file mode 100644 index 70903737..00000000 --- a/src/app/(private)/(main)/attendance/history/page.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { AttendanceHistoryContent } from '@/components/attendance'; -import type { AttendanceSummary } from '@/types/attendance'; - -// TODO: API 연동 시 실제 데이터로 교체 -const mockSummary: AttendanceSummary = { - total: 5, - attendanceCount: 3, - absenceCount: 2, - attendances: [ - { - id: 5, - status: 'ABSENT', - title: '5주차 정기모임', - start: '2026-02-23T10:00:00.000Z', - end: '2026-02-23T12:00:00.000Z', - location: '공학관 401호', - }, - { - id: 4, - status: 'ATTEND', - title: '4주차 정기모임', - start: '2026-02-16T10:00:00.000Z', - end: '2026-02-16T12:00:00.000Z', - location: '공학관 401호', - }, - { - id: 3, - status: 'ABSENT', - title: '3주차 정기모임', - start: '2026-02-09T10:00:00.000Z', - end: '2026-02-09T12:00:00.000Z', - location: '공학관 401호', - }, - { - id: 2, - status: 'ATTEND', - title: '2주차 정기모임', - start: '2026-02-02T10:00:00.000Z', - end: '2026-02-02T12:00:00.000Z', - location: '공학관 401호', - }, - { - id: 1, - status: 'ATTEND', - title: '1주차 정기모임', - start: '2026-01-26T10:00:00.000Z', - end: '2026-01-26T12:00:00.000Z', - location: '공학관 401호', - }, - ], -}; - -export default function AttendanceHistoryPage() { - return ; -} diff --git a/src/app/(private)/(main)/attendance/page.tsx b/src/app/(private)/(main)/attendance/page.tsx deleted file mode 100644 index 09050ad0..00000000 --- a/src/app/(private)/(main)/attendance/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { AttendanceContent } from '@/components/attendance'; -import type { AttendanceData } from '@/types/attendance'; - -// TODO: API 연동 시 실제 데이터로 교체 -function createMockAttendance(): AttendanceData { - const now = new Date(); - const start = now; - const end = new Date(now.getTime() + 10 * 60 * 1000); // 10분 후 - - return { - attendanceRate: 80, - title: '1주차 정기모임', - status: 'ATTEND', - code: 123456, - start: start.toISOString(), - end: end.toISOString(), - location: '공학관 401호', - }; -} - -export default function AttendancePage() { - // TODO: API 연동 시 실제 사용자 이름으로 교체 - const displayName = '사용자'; - return ; -} diff --git a/src/app/(private)/(main)/attendance/qr/page.tsx b/src/app/(private)/(main)/attendance/qr/page.tsx deleted file mode 100644 index 6c916fcd..00000000 --- a/src/app/(private)/(main)/attendance/qr/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { AttendanceQRContent } from '@/components/attendance'; - -// TODO: API 연동 시 실제 데이터로 교체 -function createMockQRData() { - const now = new Date(); - const end = new Date(now.getTime() + 10 * 60 * 1000); // 10분 후 - - return { - title: '1주차 정기모임', - code: '123456', - endTime: end.toISOString(), - }; -} - -export default function AttendanceQRPage() { - const { title, code, endTime } = createMockQRData(); - return ; -} diff --git a/src/app/(private)/(main)/board/(with-nav)/BoardNavClient.tsx b/src/app/(private)/(main)/board/(with-nav)/BoardNavClient.tsx deleted file mode 100644 index ce2d8464..00000000 --- a/src/app/(private)/(main)/board/(with-nav)/BoardNavClient.tsx +++ /dev/null @@ -1,40 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; -import { usePathname, useRouter } from 'next/navigation'; - -import { BoardNav } from '@/components/board'; -import { useActiveBoardId, useSetActiveBoardId } from '@/stores/useBoardNavStore'; -import type { BoardNavItem } from '@/types/board'; - -interface BoardNavClientProps { - items: BoardNavItem[]; -} - -function BoardNavClient({ items }: BoardNavClientProps) { - const activeBoardId = useActiveBoardId(); - const setActiveBoardId = useSetActiveBoardId(); - const pathname = usePathname(); - const router = useRouter(); - - const isDetailPage = /^\/board\/\d+$/.test(pathname); - - useEffect(() => { - if (activeBoardId === null) return; - const exists = items.some((item) => item.id === activeBoardId); - if (!exists) { - setActiveBoardId(null); - } - }, [items, activeBoardId, setActiveBoardId]); - - const handleItemSelect = (id: number | null) => { - setActiveBoardId(id); - if (isDetailPage) { - router.push('/board'); - } - }; - - return ; -} - -export { BoardNavClient }; diff --git a/src/app/(private)/(main)/board/(with-nav)/[id]/PostDetailContent.tsx b/src/app/(private)/(main)/board/(with-nav)/[id]/PostDetailContent.tsx deleted file mode 100644 index adcfdf8d..00000000 --- a/src/app/(private)/(main)/board/(with-nav)/[id]/PostDetailContent.tsx +++ /dev/null @@ -1,103 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; -import { useRouter } from 'next/navigation'; -import { Divider } from '@/components/ui'; -import { - PostCard, - PostDetailHeader, - PostActionMenu, - CommentInput, - CommentItem, - FileList, -} from '@/components/board'; -import { formatShortDateTime } from '@/lib/formatTime'; -import { toDisplayFile, isImageFileByType, mapComment } from '@/lib/board'; -import { useSetActiveBoardId } from '@/stores/useBoardNavStore'; -import { useUserId } from '@/stores/useUserStore'; -import type { PostDetail } from '@/types/board'; -import type { UploadFileItem } from '@/stores/usePostStore'; - -interface PostDetailContentProps { - post: PostDetail; -} - -function PostDetailContent({ post }: PostDetailContentProps) { - const router = useRouter(); - const currentUserId = useUserId(); - const setActiveBoardId = useSetActiveBoardId(); - - useEffect(() => { - setActiveBoardId(post.boardId); - }, [post.boardId, setActiveBoardId]); - - const isPostAuthor = currentUserId !== null && post.author.id === currentUserId; - const imageFiles = post.fileUrls - .filter((f) => isImageFileByType(f.contentType)) - .map(toDisplayFile); - const nonImageFiles = post.fileUrls - .filter((f) => !isImageFileByType(f.contentType)) - .map(toDisplayFile); - - const handleCommentSubmit = (_value: string, _file: UploadFileItem | null) => { - // TODO: 댓글 작성 API 연동 - }; - - return ( -
- - -
- - 0} - /> - {isPostAuthor && ( - router.push(`/board/edit/${post.id}`)} - onDeleted={() => router.push('/board')} - /> - )} - - - - - - - - - -
- -
- -
- - {post.comments.length > 0 && ( - <> -
- -
- -
- {post.comments.map((comment) => ( - - ))} -
- - )} -
- ); -} - -export { PostDetailContent }; diff --git a/src/app/(private)/(main)/board/(with-nav)/[id]/page.tsx b/src/app/(private)/(main)/board/(with-nav)/[id]/page.tsx deleted file mode 100644 index 0bfda59e..00000000 --- a/src/app/(private)/(main)/board/(with-nav)/[id]/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { boardServerApi } from '@/lib/apis/board.server'; -import { PostDetailContent } from './PostDetailContent'; - -interface PostDetailPageProps { - params: Promise<{ id: string }>; -} - -export default async function PostDetailPage({ params }: PostDetailPageProps) { - const { id } = await params; - //TODO:"추후 하드코딩된 clubId 제거 예정 - const response = await boardServerApi.getPostById('YUNJcjFKMO', Number(id)); - - return ; -} diff --git a/src/app/(private)/(main)/board/(with-nav)/layout.tsx b/src/app/(private)/(main)/board/(with-nav)/layout.tsx deleted file mode 100644 index 711b3b41..00000000 --- a/src/app/(private)/(main)/board/(with-nav)/layout.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import type { ReactNode } from 'react'; -import { boardServerApi } from '@/lib/apis/board.server'; -import { toBoardNavItem } from '@/lib/board'; -import { BOARD_TYPE_ORDER } from '@/constants/board/type'; -import { BoardNavClient } from './BoardNavClient'; - -interface BoardLayoutProps { - children: ReactNode; - footer: ReactNode; -} - -export default async function BoardLayout({ children, footer }: BoardLayoutProps) { - const response = await boardServerApi.getBoards('YUNJcjFKMO'); - const boards = [...response.data].sort( - (a, b) => (BOARD_TYPE_ORDER[a.type] ?? 99) - (BOARD_TYPE_ORDER[b.type] ?? 99), - ); - const items = boards.map(toBoardNavItem); - - return ( -
- - {children} -
- ); -} diff --git a/src/app/(private)/(main)/board/edit/[id]/page.tsx b/src/app/(private)/(main)/board/edit/[id]/page.tsx deleted file mode 100644 index 4e8fcf18..00000000 --- a/src/app/(private)/(main)/board/edit/[id]/page.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { notFound } from 'next/navigation'; - -import { boardServerApi } from '@/lib/apis/board.server'; -import { EditClientEditor } from './EditClientEditor'; - -interface PostEditPageProps { - params: Promise<{ id: string }>; -} - -export default async function PostEditPage({ params }: PostEditPageProps) { - const { id } = await params; - const postId = Number(id); - - if (Number.isNaN(postId)) { - notFound(); - } - - // TODO: 추후 하드코딩된 clubId 제거 예정 - const response = await boardServerApi.getPostById('YUNJcjFKMO', postId).catch(() => null); - - if (!response?.data) { - notFound(); - } - - return ( -
- -
- ); -} diff --git a/src/app/(private)/(main)/board/write/ClientEditor.tsx b/src/app/(private)/(main)/board/write/ClientEditor.tsx deleted file mode 100644 index 58412b55..00000000 --- a/src/app/(private)/(main)/board/write/ClientEditor.tsx +++ /dev/null @@ -1,50 +0,0 @@ -'use client'; - -import { useEffect, useLayoutEffect } from 'react'; - -import { CategorySelector, PostEditorShell } from '@/components/board'; -import { useBoardList } from '@/hooks'; -import { toBoardNavItem } from '@/lib/board'; -import { usePostStore } from '@/stores/usePostStore'; -import { useActiveBoardId } from '@/stores/useBoardNavStore'; - -export default function ClientEditor() { - const { data: boards } = useBoardList(); - const items = boards?.map(toBoardNavItem) ?? []; - const writableItems = items.filter((item) => item.type !== 'ALL'); - const activeBoardId = useActiveBoardId(); - - const board = usePostStore((s) => s.board); - const setBoard = usePostStore((s) => s.setBoard); - const reset = usePostStore((s) => s.reset); - - const isWritable = (id: number | null) => - id !== null && writableItems.some((item) => item.id === id); - - useLayoutEffect(() => { - reset(); - if (isWritable(activeBoardId)) { - setBoard(activeBoardId); - } - // eslint-disable-next-line react-hooks/exhaustive-deps -- 마운트 시 1회만 실행 - }, []); - - const activeId = isWritable(board) - ? board - : isWritable(activeBoardId) - ? activeBoardId - : (writableItems[0]?.id ?? null); - - useEffect(() => { - if (board !== activeId && activeId !== null) { - setBoard(activeId); - } - }, [board, activeId, setBoard]); - - return ( - } - /> - ); -} diff --git a/src/app/(private)/(main)/layout.tsx b/src/app/(private)/(main)/layout.tsx deleted file mode 100644 index c3716a34..00000000 --- a/src/app/(private)/(main)/layout.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import type { ReactNode } from 'react'; - -import { Header } from '@/components/layout'; - -export default function MainLayout({ - children, -}: Readonly<{ - children: ReactNode; -}>) { - return ( -
-
- {children} -
- ); -} diff --git a/src/app/(private)/(main)/mypage/edit/page.tsx b/src/app/(private)/(main)/mypage/edit/page.tsx deleted file mode 100644 index 32dc1bc4..00000000 --- a/src/app/(private)/(main)/mypage/edit/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { EditProfileContent } from '@/components/mypage'; - -export default function EditProfilePage() { - return ; -} diff --git a/src/app/(private)/(main)/mypage/page.tsx b/src/app/(private)/(main)/mypage/page.tsx deleted file mode 100644 index 6f9a74ac..00000000 --- a/src/app/(private)/(main)/mypage/page.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { MyPageContent } from '@/components/mypage'; - -export default function MyPagePage() { - return ; -} diff --git a/src/app/(private)/[clubId]/(main)/attendance/history/loading.tsx b/src/app/(private)/[clubId]/(main)/attendance/history/loading.tsx new file mode 100644 index 00000000..dc5fadb2 --- /dev/null +++ b/src/app/(private)/[clubId]/(main)/attendance/history/loading.tsx @@ -0,0 +1,37 @@ +import { Skeleton } from '@/components/ui'; + +export default function AttendanceHistoryLoading() { + return ( +
+
+ + +
+ +
+
+ + + +
+ + + +
+ {Array.from({ length: 4 }, (_, i) => ( +
+
+ + +
+
+ + +
+
+ ))} +
+
+
+ ); +} diff --git a/src/app/(private)/[clubId]/(main)/attendance/history/page.tsx b/src/app/(private)/[clubId]/(main)/attendance/history/page.tsx new file mode 100644 index 00000000..ea44aa29 --- /dev/null +++ b/src/app/(private)/[clubId]/(main)/attendance/history/page.tsx @@ -0,0 +1,23 @@ +import { AttendanceHistoryContent } from '@/components/attendance'; +import { attendanceServerApi } from '@/lib/apis/attendance.server'; +import type { AttendanceSummary } from '@/types/attendance'; + +interface AttendanceHistoryPageProps { + params: Promise<{ clubId: string }>; +} + +export default async function AttendanceHistoryPage({ params }: AttendanceHistoryPageProps) { + const { clubId } = await params; + + let summary: AttendanceSummary | undefined; + let errorMessage: string | undefined; + + try { + const response = await attendanceServerApi.getDetail(clubId); + summary = response.data; + } catch { + errorMessage = '출석 기록을 불러오지 못했습니다.'; + } + + return ; +} diff --git a/src/app/(private)/[clubId]/(main)/attendance/loading.tsx b/src/app/(private)/[clubId]/(main)/attendance/loading.tsx new file mode 100644 index 00000000..15f3a585 --- /dev/null +++ b/src/app/(private)/[clubId]/(main)/attendance/loading.tsx @@ -0,0 +1,21 @@ +import { Skeleton } from '@/components/ui'; + +export default function AttendanceLoading() { + return ( +
+
+ +
+ +
+ + +
+ +
+ + +
+
+ ); +} diff --git a/src/app/(private)/[clubId]/(main)/attendance/page.tsx b/src/app/(private)/[clubId]/(main)/attendance/page.tsx new file mode 100644 index 00000000..0b9194d3 --- /dev/null +++ b/src/app/(private)/[clubId]/(main)/attendance/page.tsx @@ -0,0 +1,32 @@ +import { AttendanceContent } from '@/components/attendance'; +import { attendanceServerApi } from '@/lib/apis/attendance.server'; +import type { AttendanceData } from '@/types/attendance'; + +interface AttendancePageProps { + params: Promise<{ clubId: string }>; + searchParams: Promise<{ sessionId?: string; code?: string }>; +} + +export default async function AttendancePage({ params, searchParams }: AttendancePageProps) { + const { clubId } = await params; + const { sessionId: qrSessionId, code: qrCode } = await searchParams; + + let attendance: AttendanceData | undefined; + let errorMessage: string | undefined; + + try { + const response = await attendanceServerApi.getAttendance(clubId); + attendance = response.data; + } catch { + errorMessage = '출석 정보를 불러오지 못했습니다.'; + } + + return ( + + ); +} diff --git a/src/app/(private)/[clubId]/(main)/attendance/qr/loading.tsx b/src/app/(private)/[clubId]/(main)/attendance/qr/loading.tsx new file mode 100644 index 00000000..bf431daa --- /dev/null +++ b/src/app/(private)/[clubId]/(main)/attendance/qr/loading.tsx @@ -0,0 +1,29 @@ +import { Skeleton } from '@/components/ui'; + +export default function AttendanceQRLoading() { + return ( +
+
+ +
+ +
+
+ + +
+ +
+
+ +
+ + +
+ +
+
+
+
+ ); +} diff --git a/src/app/(private)/[clubId]/(main)/attendance/qr/page.tsx b/src/app/(private)/[clubId]/(main)/attendance/qr/page.tsx new file mode 100644 index 00000000..b36f6462 --- /dev/null +++ b/src/app/(private)/[clubId]/(main)/attendance/qr/page.tsx @@ -0,0 +1,28 @@ +import { redirect } from 'next/navigation'; + +import { AttendanceQRContent } from '@/components/attendance'; +import { homeServerApi } from '@/lib/apis/home.server'; + +interface AttendanceQRPageProps { + params: Promise<{ clubId: string }>; + searchParams: Promise<{ sessionId?: string }>; +} + +export default async function AttendanceQRPage({ params, searchParams }: AttendanceQRPageProps) { + const { clubId } = await params; + const { sessionId: rawSessionId } = await searchParams; + const sessionId = Number(rawSessionId); + + if (!Number.isInteger(sessionId) || sessionId <= 0) { + redirect(`/${clubId}/attendance`); + } + + const { data } = await homeServerApi.getDashboard(clubId); + const role = data.myInfo.userInfo.role; + + if (role !== 'LEAD' && role !== 'ADMIN') { + redirect(`/${clubId}/attendance`); + } + + return ; +} diff --git a/src/app/(private)/(main)/board/(with-nav)/@footer/[id]/page.tsx b/src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/[postId]/page.tsx similarity index 100% rename from src/app/(private)/(main)/board/(with-nav)/@footer/[id]/page.tsx rename to src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/[postId]/page.tsx diff --git a/src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/page.tsx b/src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/page.tsx new file mode 100644 index 00000000..736c0dab --- /dev/null +++ b/src/app/(private)/[clubId]/(main)/board/(with-nav)/@footer/[boardId]/page.tsx @@ -0,0 +1,5 @@ +import { Footer } from '@/components/layout'; + +export default function BoardByIdFooter() { + return