Skip to content

feat: 방 생성 API 연동#92

Merged
ljh130334 merged 7 commits intodevelopfrom
feat/api-rooms-makeroom
Aug 12, 2025
Merged

feat: 방 생성 API 연동#92
ljh130334 merged 7 commits intodevelopfrom
feat/api-rooms-makeroom

Conversation

@ljh130334
Copy link
Member

@ljh130334 ljh130334 commented Aug 12, 2025

#️⃣ 연관된 이슈

#83

📝 작업 내용

방 생성 API 연동을 구현했습니다.

🕸️ 주요 구현 사항

1. 방 생성 API 연동

  • POST /rooms API 연동
  • 요청/응답 타입 정의 (@/types/room.ts)
  • JWT 토큰 기반 인증 처리

2. 폼 검증 및 데이터 처리

  • 4자리 숫자 비밀번호 검증 (비공개방)
  • 공개방: password: null, 비공개방: password: "1234"
  • 날짜 형식 변환 (YYYY.MM.DD)
  • 실시간 폼 유효성 검사

3. 날짜 시스템 개선

  • 기본 시작일: 오늘 + 1일 (자동 설정)
  • 기본 종료일: 오늘 + 1일 (자동 설정)
  • 날짜 검증 로직 (과거 날짜 방지)

4. UX 개선

  • 로딩 상태 표시 ("생성 중...")
  • 중복 실행 방지
  • 생성 완료 후 올바른 페이지 이동

🔧 기술적 개선사항

  • Passive Event Listener 오류 해결: DateWheel 컴포넌트 터치 이벤트 최적화

Summary by CodeRabbit

  • 새로운 기능
    • 그룹 생성이 서버와 연결되어 실제로 방을 생성할 수 있습니다. 필수 항목 검증, 날짜 유효성 검사, 오류 알림, 생성 성공 시 상세 화면으로 이동을 제공합니다.
    • 제출 상태 표시가 추가되어 생성 중에는 버튼 문구가 “생성 중...”으로 변경되고 중복 제출이 방지됩니다.
  • 리팩터
    • 날짜 휠 상호작용이 개선되어 터치/스크롤/마우스 드래그 입력에서 더 안정적이고 일관된 동작을 제공합니다. 의도치 않은 스크롤을 줄이고 응답성을 향상했습니다.

@ljh130334 ljh130334 added the 📬 API 서버 API 통신 label Aug 12, 2025
@vercel
Copy link

vercel bot commented Aug 12, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Project Deployment Preview Comments Updated (UTC)
thip Ready Preview 💬 Add feedback Aug 12, 2025 7:31am

@coderabbitai
Copy link

coderabbitai bot commented Aug 12, 2025

Walkthrough

개발용 토큰 상수 변경, 방 생성 타입과 API 헬퍼 추가, 그룹 생성 페이지에 방 생성 플로우 통합 및 유효성 검증/에러 처리 도입, DateWheel 터치/휠 입력 처리 로직을 useCallback/useEffect 기반으로 재구성하여 비수동 이벤트 리스너로 교체. 공개 API 시그니처 변경 없음.

Changes

Cohort / File(s) Change Summary
API 인증 설정
src/api/index.ts
개발용 ACCESS_TOKEN 값만 갱신. 인터셉터, 에러 처리, 공개 export 변화 없음.
Room API + 타입 도입
src/api/rooms/createRoom.ts, src/types/room.ts
방 생성 요청/응답 타입(CreateRoomRequest, CreateRoomData, ApiResponse<T>) 등 Room 관련 인터페이스 추가. createRoom(roomData) 헬퍼 신설(POST 'rooms').
그룹 생성 페이지 통합
src/pages/group/CreateGroup.tsx
방 생성 API 연동, 입력값 검증(ISBN/카테고리/이름/설명/기간/정원/비밀번호), 동적 기본 날짜, 제출 상태 관리, 성공 시 상세 화면 이동/실패 알림 처리. 내부 Book 타입 도입.
DateWheel 입력 리팩터링
src/components/creategroup/ActivityPeriodSection/DateWheel.tsx
터치/휠 비수동 이벤트 리스너로 전환, 핸들러 useCallback화, moveToIndex 헬퍼로 이동 로직 집중, onChange 호출 타이밍 정리. 외부 props 시그니처 변화 없음.

Sequence Diagram(s)

sequenceDiagram
  participant User as User
  participant UI as CreateGroup Page
  participant Helper as createRoom()
  participant API as apiClient
  participant BE as Server

  User->>UI: 제출 클릭
  UI->>UI: 입력값 검증/페이로드 구성
  UI->>Helper: createRoom(payload)
  Helper->>API: POST /rooms
  API->>BE: 요청 전송
  BE-->>API: ApiResponse<CreateRoomData>
  API-->>Helper: 응답 전달
  Helper-->>UI: isSuccess, roomId
  alt 성공
    UI->>UI: isSubmitting 해제 및 네비게이션
  else 실패
    UI->>User: 에러 알림
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20–25 minutes

Possibly related issues

Poem

눈발 같은 픽셀 위, 토끼가 뛰네 🐇
새 방의 문을 톡, API에 열쇠를 더해
날짜 바퀴 사각사각, 손끝에 맞춰 돌고
토큰은 새로 깔끔, 요청은 반짝 직진
삐빅—성공! 방으로 폴짝 들어가네 ✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/api-rooms-makeroom

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (7)
src/components/creategroup/ActivityPeriodSection/DateWheel.tsx (2)

69-90: 드래그 이동 시 연속 렌더링 최적화 제안 (rAF/쓰로틀링).

매 이동마다 values.map으로 sliderState를 재생성하고 상태 업데이트를 트리거합니다. 트랙패드/손가락 드래그에서 잦은 리렌더가 발생할 수 있으니 requestAnimationFrame 기반으로 업데이트를 모아서 처리하거나 간단한 쓰로틀을 적용하는 것을 권장합니다. 또한 sensitivity는 상수로 컴포넌트 외부에 선언하거나 prop으로 노출하여 튜닝 가능하게 하는 것도 좋습니다.

예시(핵심 아이디어만 제시):

const SENSITIVITY = 30;
const rAFRef = useRef<number | null>(null);

const scheduleUpdate = (newIndex: number) => {
  if (rAFRef.current) cancelAnimationFrame(rAFRef.current);
  rAFRef.current = requestAnimationFrame(() => {
    setCurrentIndex(newIndex);
    setSliderState({
      abs: newIndex,
      slides: values.map((_, i) => ({ distance: i - newIndex })),
    });
  });
};

99-116: 중복 로직 제거로 간결화 제안.

handleInteractionMovemoveToIndex에서 sliderState 재구성 로직이 중복입니다. updateToIndex(newIndex: number, triggerOnChange: boolean) 헬퍼로 추출하면 가독성과 유지보수성이 좋아집니다.

적용 방향(요지):

- if (newIndex !== currentIndex) {
-   setCurrentIndex(newIndex);
-   onChange(values[newIndex]);
-   const newState: SliderState = { abs: newIndex, slides: values.map((_, i) => ({ distance: i - newIndex })) };
-   setSliderState(newState);
- }

+ const updateToIndex = (newIndex: number, triggerChange: boolean) => {
+   if (newIndex === currentIndex) return;
+   setCurrentIndex(newIndex);
+   setSliderState({ abs: newIndex, slides: values.map((_, i) => ({ distance: i - newIndex })) });
+   if (triggerChange) onChange(values[newIndex]);
+ };

 // moveToIndex 내부
- if (newIndex !== currentIndex) { ... }
+ updateToIndex(newIndex, true);

 // handleInteractionMove 내부
- if (newIndex !== currentIndex) { ... }
+ updateToIndex(newIndex, false);
src/types/room.ts (2)

27-31: 날짜 문자열 형식 안정성을 타입으로 보강 제안.

YYYY.MM.DD 포맷을 템플릿 리터럴 타입으로 표현하면 컴파일 단계에서 형식 오류를 더 일찍 잡을 수 있습니다.

아래와 같이 보강을 고려해 보세요.

+export type DateYMD = `${number}.${number}.${number}`;
+
 export interface CreateRoomRequest {
   isbn: string;
   category: string;
   roomName: string;
   description: string;
-  progressStartDate: string; // YYYY.MM.DD 형식
-  progressEndDate: string; // YYYY.MM.DD 형식
+  progressStartDate: DateYMD; // YYYY.MM.DD 형식
+  progressEndDate: DateYMD; // YYYY.MM.DD 형식
   recruitCount: number;
   password: string | null; // 비공개방: 4자리 숫자, 공개방: null
   isPublic: boolean;
 }

63-69: 실패 응답에서 data 필드 null/생략 가능성 고려.

백엔드가 실패 시 data를 생략하거나 null로 내려줄 수 있습니다. 현재는 data: T로 고정되어 있어 런타임과 타입이 어긋날 수 있습니다.

백엔드 계약이 확정되지 않았다면 다음과 같이 완화하는 것을 권장합니다.

 export interface ApiResponse<T> {
   isSuccess: boolean;
   code: number;
   message: string;
-  data: T;
+  data?: T | null;
 }

확인을 위해 서버 스펙/샘플 응답(JSON)을 재확인해 주세요. 필요 시 제가 문서화/타입 정리를 도와드릴 수 있습니다.

src/api/rooms/createRoom.ts (2)

8-11: 반환 타입 명시로 가독성 및 유지보수성 향상.

함수 반환 타입을 명시하면 호출부에서 타입 추론이 더 명확해집니다.

다음과 같이 타입을 명시하세요.

-export const createRoom = async (roomData: CreateRoomRequest) => {
+export const createRoom = async (roomData: CreateRoomRequest): Promise<CreateRoomResponse> => {
   const response = await apiClient.post<CreateRoomResponse>('rooms', roomData);
   return response.data;
 };

13-40: 사용 예시는 코드 파일보다 문서/주석 상단(JSDoc)으로 이동 권장.

길이가 있는 사용 예시는 README나 JSDoc에 두는 편이 코드 탐색 시 잡음을 줄여줍니다. 필요하다면 간단한 JSDoc 예시만 남기고 상세 흐름은 문서로 옮겨 주세요.

src/pages/group/CreateGroup.tsx (1)

10-10: 중복 슬래시 경로 수정.

임포트 경로에 중복 슬래시가 있습니다. 일부 번들러에서는 문제 없지만 일관성 있게 정리하는 것이 좋습니다.

-import PrivacySettingSection from '../../components/creategroup//PrivacySettingSection/PrivacySettingSection';
+import PrivacySettingSection from '../../components/creategroup/PrivacySettingSection/PrivacySettingSection';
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 44ac2fd and 18090c2.

📒 Files selected for processing (5)
  • src/api/index.ts (1 hunks)
  • src/api/rooms/createRoom.ts (1 hunks)
  • src/components/creategroup/ActivityPeriodSection/DateWheel.tsx (3 hunks)
  • src/pages/group/CreateGroup.tsx (3 hunks)
  • src/types/room.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (2)
src/api/rooms/createRoom.ts (2)
src/types/room.ts (3)
  • ApiResponse (64-69)
  • CreateRoomData (35-37)
  • CreateRoomRequest (22-32)
src/api/index.ts (1)
  • apiClient (27-33)
src/pages/group/CreateGroup.tsx (3)
src/types/book.ts (1)
  • Book (1-7)
src/types/room.ts (1)
  • CreateRoomRequest (22-32)
src/api/rooms/createRoom.ts (1)
  • createRoom (8-11)
🪛 Gitleaks (8.27.2)
src/api/index.ts

5-5: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🔇 Additional comments (1)
src/components/creategroup/ActivityPeriodSection/DateWheel.tsx (1)

136-175: 비수동(non-passive) 터치/휠 리스너 등록 및 정리(Cleanup) 처리, 구현 잘되었습니다.

preventDefault가 필요한 UX 요구사항을 충족하면서 메모리 누수 없이 적절히 해제하고 있습니다.

Comment on lines 4 to 6
const ACCESS_TOKEN =
'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsImlhdCI6MTc1NDIwMTY4OCwiZXhwIjoxNzU2NzkzNjg4fQ.oOyJ7JI_t2-Xq1-gfAv4ZaYNrbyplvqdxhCk76-Txe4';
'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjksImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.CCb_F6OGe02_ITYsE-tqc2_PvSkRsxd96t8NWNIa1pI';

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

하드코딩된 JWT 토큰 커밋은 즉시 제거 필요 (비밀정보 유출).

Gitleaks가 JWT를 감지했습니다. 프런트 코드에 토큰을 하드코딩/커밋하면 누구나 악용할 수 있습니다. 로컬스토리지의 사용자 토큰만 사용하도록 수정하고, 개발 편의를 위한 토큰이 필요하다면 최소한 커밋에서 제거하고 개인 로컬 환경변수로 주입하세요. 프런트의 VITE_ 환경변수는 번들에 포함되므로 근본적 보안수단이 아님을 유의하세요.

아래와 같이 ACCESS_TOKEN 상수 및 fallback 로직을 제거하세요.

-// 하드코딩된 액세스 토큰
-const ACCESS_TOKEN =
-  'Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjksImlhdCI6MTc1NDM4MjY1MiwiZXhwIjoxNzU2OTc0NjUyfQ.CCb_F6OGe02_ITYsE-tqc2_PvSkRsxd96t8NWNIa1pI';
 apiClient.interceptors.request.use(
   config => {
     // 로컬스토리지에서 토큰 먼저 확인
     const token = TokenManager.getAccessToken();
     if (token) {
       config.headers.Authorization = `Bearer ${token}`;
-    } else {
-      // 토큰이 없으면 하드코딩된 토큰 사용 (개발용)
-      config.headers.Authorization = ACCESS_TOKEN;
     }
     return config;
   },
   error => Promise.reject(error),
 );

Also applies to: 39-46

🧰 Tools
🪛 Gitleaks (8.27.2)

5-5: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🤖 Prompt for AI Agents
In src/api/index.ts around lines 4-6 (and similarly lines 39-46), there is a
hardcoded JWT ACCESS_TOKEN constant and fallback logic that must be removed;
delete the constant and any fallback that injects a static token, and instead
read the authenticated token only from localStorage (or from a non-bundled,
developer-only runtime source) at request time; ensure code fails gracefully
when no token is present (return unauthorized / prompt login) and remove the
hardcoded secret from the repo (and any tests/fixtures), avoiding use of VITE_*
env vars for this secret.

Comment on lines +16 to +23
// Book 타입 정의
interface Book {
id?: number;
title: string;
author: string;
cover: string;
isbn?: string;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Book 타입 재정의 대신 공용 타입 임포트 사용.

동일 의미의 타입을 파일 내 재정의하면 추후 스키마 변경 시 불일치가 발생합니다. src/types/book.ts에 정의된 공용 타입을 사용하세요.

-import type { CreateRoomRequest } from '@/types/room';
-
-// Book 타입 정의
-interface Book {
-  id?: number;
-  title: string;
-  author: string;
-  cover: string;
-  isbn?: string;
-}
+import type { CreateRoomRequest } from '@/types/room';
+import type { Book } from '@/types/book';
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Book 타입 정의
interface Book {
id?: number;
title: string;
author: string;
cover: string;
isbn?: string;
}
-import type { CreateRoomRequest } from '@/types/room';
-// Book 타입 정의
-interface Book {
- id?: number;
- title: string;
- author: string;
- cover: string;
- isbn?: string;
-}
+import type { CreateRoomRequest } from '@/types/room';
+import type { Book } from '@/types/book';
🤖 Prompt for AI Agents
In src/pages/group/CreateGroup.tsx around lines 16 to 23, the local Book
interface is duplicating the shared type; remove this local definition and
instead import the Book type from src/types/book.ts (add an import at the top:
import { Book } from 'src/types/book' or the correct relative path), then update
any references to use the imported Book type; ensure there are no remaining
duplicate definitions or naming conflicts.

Comment on lines +32 to +52
const getDefaultDates = () => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);

const oneMonthLater = new Date(today);
oneMonthLater.setDate(today.getDate() + 1);

return {
start: {
year: tomorrow.getFullYear(),
month: tomorrow.getMonth() + 1,
day: tomorrow.getDate(),
},
end: {
year: oneMonthLater.getFullYear(),
month: oneMonthLater.getMonth() + 1,
day: oneMonthLater.getDate(),
},
};
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

기본 종료일(oneMonthLater) 계산 버그: 내일과 동일하게 설정됩니다.

oneMonthLatertoday + 1일로 설정되어 시작일과 동일한 날짜가 됩니다. 한 달 후로 의도했다면 월 단위 증가로 보정하세요.

-  const getDefaultDates = () => {
+  const getDefaultDates = () => {
     const today = new Date();
     const tomorrow = new Date(today);
     tomorrow.setDate(today.getDate() + 1);
 
-    const oneMonthLater = new Date(today);
-    oneMonthLater.setDate(today.getDate() + 1);
+    const oneMonthLater = new Date(tomorrow);
+    // 한 달 후로 설정 (달 증가; 일자는 내일과 동일하게 유지)
+    oneMonthLater.setMonth(tomorrow.getMonth() + 1);
 
     return {
       start: {
         year: tomorrow.getFullYear(),
         month: tomorrow.getMonth() + 1,
         day: tomorrow.getDate(),
       },
       end: {
         year: oneMonthLater.getFullYear(),
         month: oneMonthLater.getMonth() + 1,
         day: oneMonthLater.getDate(),
       },
     };
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getDefaultDates = () => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
const oneMonthLater = new Date(today);
oneMonthLater.setDate(today.getDate() + 1);
return {
start: {
year: tomorrow.getFullYear(),
month: tomorrow.getMonth() + 1,
day: tomorrow.getDate(),
},
end: {
year: oneMonthLater.getFullYear(),
month: oneMonthLater.getMonth() + 1,
day: oneMonthLater.getDate(),
},
};
};
const getDefaultDates = () => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(today.getDate() + 1);
const oneMonthLater = new Date(tomorrow);
// 한 달 후로 설정 (달 증가; 일자는 내일과 동일하게 유지)
oneMonthLater.setMonth(tomorrow.getMonth() + 1);
return {
start: {
year: tomorrow.getFullYear(),
month: tomorrow.getMonth() + 1,
day: tomorrow.getDate(),
},
end: {
year: oneMonthLater.getFullYear(),
month: oneMonthLater.getMonth() + 1,
day: oneMonthLater.getDate(),
},
};
};
🤖 Prompt for AI Agents
In src/pages/group/CreateGroup.tsx around lines 32 to 52, the oneMonthLater date
is incorrectly set to today + 1 day (same as tomorrow); change the logic to add
one calendar month instead of one day. Specifically, after creating
oneMonthLater as a copy of today, call setMonth(today.getMonth() + 1) (or use an
equivalent month-add operation that preserves day/year rollover) so the end date
becomes one month ahead of today rather than identical to the start date.

Comment on lines +68 to +93
const handleCompleteClick = async () => {
if (isSubmitting) return; // 중복 실행 방지

setIsSubmitting(true);

try {
// 날짜 형식 변환 (YYYY.MM.DD)
const formatDate = (date: { year: number; month: number; day: number }) => {
const month = date.month.toString().padStart(2, '0');
const day = date.day.toString().padStart(2, '0');
return `${date.year}.${month}.${day}`;
};

// 방 생성 요청 데이터 구성
const roomData: CreateRoomRequest = {
isbn: selectedBook?.isbn || '9788936434632', // 선택된 책의 ISBN 또는 기본값
category: selectedGenre,
roomName: roomTitle.trim(),
description: roomDescription.trim(),
progressStartDate: formatDate(startDate),
progressEndDate: formatDate(endDate),
recruitCount: memberLimit,
password: isPrivate ? password.trim() : null,
isPublic: !isPrivate, // isPrivate의 반대값
};

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

도서 선택 누락 시 기본 ISBN 하드코딩 사용은 위험 — 명시적 검증으로 대체.

선택 도서가 없을 때 임의의 기본 ISBN을 사용하는 것은 데이터 무결성을 깨뜨립니다. 생성 진행 전 도서/ISBN을 강제 검증하고, roomData 생성 시 널 단언이 아닌 타입 보장을 사용하세요.

-  const handleCompleteClick = async () => {
+  const handleCompleteClick = async () => {
     if (isSubmitting) return; // 중복 실행 방지
 
     setIsSubmitting(true);
 
     try {
+      // 필수 입력 선검증: 도서/ISBN
+      if (!selectedBook?.isbn) {
+        alert('도서를 선택해주세요.');
+        return;
+      }
+
       // 날짜 형식 변환 (YYYY.MM.DD)
       const formatDate = (date: { year: number; month: number; day: number }) => {
         const month = date.month.toString().padStart(2, '0');
         const day = date.day.toString().padStart(2, '0');
         return `${date.year}.${month}.${day}`;
       };
 
       // 방 생성 요청 데이터 구성
       const roomData: CreateRoomRequest = {
-        isbn: selectedBook?.isbn || '9788936434632', // 선택된 책의 ISBN 또는 기본값
+        isbn: selectedBook.isbn, // 선택된 책의 ISBN (필수)
         category: selectedGenre,
         roomName: roomTitle.trim(),
         description: roomDescription.trim(),
         progressStartDate: formatDate(startDate),
         progressEndDate: formatDate(endDate),
         recruitCount: memberLimit,
         password: isPrivate ? password.trim() : null,
         isPublic: !isPrivate, // isPrivate의 반대값
       };

비고: 가능하면 setIsSubmitting(true)는 선검증(도서/날짜/기타) 이후에 호출하는 것이 UX에 유리합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleCompleteClick = async () => {
if (isSubmitting) return; // 중복 실행 방지
setIsSubmitting(true);
try {
// 날짜 형식 변환 (YYYY.MM.DD)
const formatDate = (date: { year: number; month: number; day: number }) => {
const month = date.month.toString().padStart(2, '0');
const day = date.day.toString().padStart(2, '0');
return `${date.year}.${month}.${day}`;
};
// 방 생성 요청 데이터 구성
const roomData: CreateRoomRequest = {
isbn: selectedBook?.isbn || '9788936434632', // 선택된 책의 ISBN 또는 기본값
category: selectedGenre,
roomName: roomTitle.trim(),
description: roomDescription.trim(),
progressStartDate: formatDate(startDate),
progressEndDate: formatDate(endDate),
recruitCount: memberLimit,
password: isPrivate ? password.trim() : null,
isPublic: !isPrivate, // isPrivate의 반대값
};
const handleCompleteClick = async () => {
if (isSubmitting) return; // 중복 실행 방지
setIsSubmitting(true);
try {
// 필수 입력 선검증: 도서/ISBN
if (!selectedBook?.isbn) {
alert('도서를 선택해주세요.');
return;
}
// 날짜 형식 변환 (YYYY.MM.DD)
const formatDate = (date: { year: number; month: number; day: number }) => {
const month = date.month.toString().padStart(2, '0');
const day = date.day.toString().padStart(2, '0');
return `${date.year}.${month}.${day}`;
};
// 방 생성 요청 데이터 구성
const roomData: CreateRoomRequest = {
isbn: selectedBook.isbn, // 선택된 책의 ISBN (필수)
category: selectedGenre,
roomName: roomTitle.trim(),
description: roomDescription.trim(),
progressStartDate: formatDate(startDate),
progressEndDate: formatDate(endDate),
recruitCount: memberLimit,
password: isPrivate ? password.trim() : null,
isPublic: !isPrivate, // isPrivate의 반대값
};
🤖 Prompt for AI Agents
In src/pages/group/CreateGroup.tsx around lines 68 to 93, replace the current
approach of falling back to a hardcoded ISBN by adding an explicit validation
that selectedBook (and its isbn) exists before building roomData; if missing,
surface a user-facing error (toast or form error) and return early without
setting isSubmitting, then only call setIsSubmitting(true) after all validations
pass; when constructing roomData, use the validated selectedBook.isbn (no
hardcoded default) and ensure types reflect optional password/null properly.

Comment on lines +111 to +121
// 요청 데이터 검증
const validation = {
isbn: roomData.isbn.length > 0,
category: roomData.category.length > 0,
roomName: roomData.roomName.length > 0,
description: roomData.description.length > 0,
startDate: roomData.progressStartDate.length >= 8,
endDate: roomData.progressEndDate.length >= 8,
recruitCount: roomData.recruitCount >= 1 && roomData.recruitCount <= 30,
password: !isPrivate || (roomData.password !== null && /^\d{4}$/.test(roomData.password)),
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

날짜 문자열 길이 검증이 부정확합니다.

YYYY.MM.DD는 길이 10입니다. 현재는 >= 8로 허용 폭이 너무 넓습니다.

-        startDate: roomData.progressStartDate.length >= 8,
-        endDate: roomData.progressEndDate.length >= 8,
+        startDate: roomData.progressStartDate.length === 10,
+        endDate: roomData.progressEndDate.length === 10,

추가로 정규식 검증까지 고려하면 더욱 견고해집니다: /^\d{4}\.\d{2}\.\d{2}$/

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 요청 데이터 검증
const validation = {
isbn: roomData.isbn.length > 0,
category: roomData.category.length > 0,
roomName: roomData.roomName.length > 0,
description: roomData.description.length > 0,
startDate: roomData.progressStartDate.length >= 8,
endDate: roomData.progressEndDate.length >= 8,
recruitCount: roomData.recruitCount >= 1 && roomData.recruitCount <= 30,
password: !isPrivate || (roomData.password !== null && /^\d{4}$/.test(roomData.password)),
};
// 요청 데이터 검증
const validation = {
isbn: roomData.isbn.length > 0,
category: roomData.category.length > 0,
roomName: roomData.roomName.length > 0,
description: roomData.description.length > 0,
startDate: roomData.progressStartDate.length === 10,
endDate: roomData.progressEndDate.length === 10,
recruitCount: roomData.recruitCount >= 1 && roomData.recruitCount <= 30,
password: !isPrivate || (roomData.password !== null && /^\d{4}$/.test(roomData.password)),
};
🤖 Prompt for AI Agents
In src/pages/group/CreateGroup.tsx around lines 111 to 121, the date length
checks (>= 8) are too permissive for the expected "YYYY.MM.DD" format; update
the validation for startDate and endDate to require exact length 10 and validate
against a strict regex /^\d{4}\.\d{2}\.\d{2}$/ so only properly formatted dates
pass (e.g., change the checks to ensure roomData.progressStartDate and
roomData.progressEndDate are length 10 and match the regex).

Comment on lines +135 to +147
// 두 가지 응답 형식 모두 확인
const isSuccessful = response.isSuccess || response.isSuccess;

if (isSuccessful) {
// 성공 시 모집 중인 방 상세 페이지로 이동
navigate('/group/detail', {
replace: true,
state: { roomId: response.data.roomId },
});
} else {
alert(`방 생성에 실패했습니다: ${response.message} (코드: ${response.code})`);
}
} catch (error) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

성공 판정 로직 오타: 항상 동일 필드만 확인합니다.

const isSuccessful = response.isSuccess || response.isSuccess;는 중복입니다. 만약 백엔드가 success 또는 isSuccess를 혼용한다면 아래처럼 보수적으로 처리하세요.

-      // 두 가지 응답 형식 모두 확인
-      const isSuccessful = response.isSuccess || response.isSuccess;
+      // 응답 형식 호환 처리
+      const isSuccessful = (response as any).success ?? response.isSuccess === true;

가능하면 서버 계약을 단일 키로 정리하는 것을 권장합니다.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// 두 가지 응답 형식 모두 확인
const isSuccessful = response.isSuccess || response.isSuccess;
if (isSuccessful) {
// 성공 시 모집 중인 방 상세 페이지로 이동
navigate('/group/detail', {
replace: true,
state: { roomId: response.data.roomId },
});
} else {
alert(`방 생성에 실패했습니다: ${response.message} (코드: ${response.code})`);
}
} catch (error) {
// 응답 형식 호환 처리
const isSuccessful = (response as any).success ?? response.isSuccess === true;
if (isSuccessful) {
// 성공 시 모집 중인 방 상세 페이지로 이동
navigate('/group/detail', {
replace: true,
state: { roomId: response.data.roomId },
});
} else {
alert(`방 생성에 실패했습니다: ${response.message} (코드: ${response.code})`);
}
} catch (error) {
🤖 Prompt for AI Agents
In src/pages/group/CreateGroup.tsx around lines 135 to 147, the success check
currently reads the same field twice causing a bug; replace the duplicated check
with a robust check that accepts either response.success or response.isSuccess
(and optionally a nested success flag like response.data.success) and treat
truthy values as success, then proceed to navigate on success and show the
existing alert on failure; prefer normalizing the server response to a single
key or map the server fields to a single local boolean before branching.

Comment on lines +139 to +144
// 성공 시 모집 중인 방 상세 페이지로 이동
navigate('/group/detail', {
replace: true,
state: { roomId: response.data.roomId },
});
} else {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

라우팅 경로/파라미터 전달 방식 확인 필요.

/group/detail로 state에 roomId를 담아 이동합니다. 라우트가 /:roomId 파라미터를 기대하거나 state 미사용 방식이라면 페이지 진입 후 데이터 로딩이 실패할 수 있습니다.

아래 스크립트로 라우트 정의 및 사용처를 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# 라우트 정의에서 group/detail 경로 검색
rg -n --glob "src/**" "group/detail" -A 5 -B 5

# roomId를 URL 파라미터로 받는지 확인
rg -n --glob "src/**" "path:\\s*['\"]/group/detail" -A 3 -B 3

# location.state.roomId 사용 여부 검색
rg -n --glob "src/**" "state:\\s*\\{[^}]*roomId" -A 2 -B 2

Length of output: 2290


🏁 Script executed:

#!/bin/bash
# Find and display the top of GroupDetail.tsx to inspect parameter handling
file=$(rg --files | grep -E 'src/pages/.*/GroupDetail\.tsx$' || true)
if [ -z "$file" ]; then
  echo "GroupDetail.tsx not found"
  exit 1
fi
echo "Located file: $file"
sed -n '1,200p' "$file"

Length of output: 3973


라우팅 경로 및 파라미터 처리 로직 확인 및 보완 필요
현재 /group/detail 경로는 URL 파라미터를 받지 않고, GroupDetail.tsx 에서도 location.state.roomId 또는 useParams() 를 통해 roomId를 읽어오는 로직이 없습니다.
이대로 두면 navigate('/group/detail', { state: { roomId } }) 로 이동해도 상세 데이터를 불러오지 못합니다.

조치 사항:

  • 경로를 /group/detail/:roomId 처럼 URL 파라미터로 변경하거나
  • GroupDetail.tsx 에서 useLocation() 또는 useParams() 를 사용해 roomId를 읽고, 해당 ID로 API 호출하도록 구현

대상 파일:

  • src/pages/index.tsx (Route 정의)
  • src/pages/groupDetail/GroupDetail.tsx (파라미터/상태 처리 로직)
🤖 Prompt for AI Agents
In src/pages/group/CreateGroup.tsx around lines 139 to 144, the navigate call
pushes state.roomId to '/group/detail' but the app routes and GroupDetail
component don't read URL params or location.state so the detail page will not
load data; update routing to use '/group/detail/:roomId' in src/pages/index.tsx
and change the navigate call to include the roomId in the URL (e.g.
navigate(`/group/detail/${response.data.roomId}`, { replace: true })) OR keep
the current URL and implement roomId reading in
src/pages/groupDetail/GroupDetail.tsx by using useLocation() to read
location.state.roomId with a fallback to useParams().roomId, then use that ID to
fetch the detail API; ensure both route definition and GroupDetail data-loading
logic support the chosen approach.

@ljh130334 ljh130334 merged commit a4b41ed into develop Aug 12, 2025
3 checks passed
@ljh130334 ljh130334 deleted the feat/api-rooms-makeroom branch August 13, 2025 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 API 통신

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant