Skip to content

여행 그룹 생성#10

Merged
SongMinQQ merged 12 commits into
developfrom
feat/generate-group
Feb 24, 2026
Merged

여행 그룹 생성#10
SongMinQQ merged 12 commits into
developfrom
feat/generate-group

Conversation

@SongMinQQ

Copy link
Copy Markdown
Contributor

#️⃣연관된 이슈

#7

📝작업 내용

1. 구현 목적

  • 그룹 생성 폼의 책임을 분리하고, 그룹 생성 API/파일 업로드 API 책임을 명확히 분리함
  • 이미지가 있을 때만 업로드(발급 → PUT → 완료 확인) 후 그룹 생성이 진행되도록 보장

2. 컴포넌트 구조 정리

  • GroupGenerateForm: 상태 조합/전달 역할(컨테이너)
  • GroupGenerateRequiredFields: 필수 입력 UI(그룹명, 설명, 최대 인원, 공개 여부)
  • GroupGenerateOptionalFields: 선택 입력 UI(그룹 이미지)
  • GroupGenerateSubmitBtn: 폼 제출 로직 전담

3. 폼 상태 모델링

  • requiredForm / optionalForm으로 분리 관리
  • 제출 버튼 props도 개별 필드 나열 대신 묶음 객체로 전달

4. 상수화

  • 그룹 생성 폼 제약값을 상수로 관리
  • 상수 항목
    • 그룹명 최대 길이
    • 그룹 설명 최대 길이
    • 그룹 최소/최대/기본 인원
  • 적용 위치
    • features/group/model/groupGenerateForm.ts
    • features/group/GroupGenerateForm.tsx
    • features/group/ui/GroupGenerateRequiredFields.tsx

5. API 책임 분리

  • 기존: shared/api/group/generateGroup.ts 내부에 파일 업로드 로직 포함
  • 변경:
    • shared/api/file/uploadFile.ts 생성
    • 공용 함수 uploadFile(file)에서 presigned 발급/PUT/완료 확인 처리
    • generateGroup은 그룹 생성에만 집중하고, 필요 시 uploadFile 호출해 key 사용

6. 테스트

  • Playwright 시나리오: tests/e2e/group-create.spec.js
  • Success Case
    • 이미지 업로드(발급/PUT/완료) 이후 그룹 생성 호출
  • Fail Case
    • upload-complete 실패 시 그룹 생성 요청이 호출되지 않음
  • 결과: 2 passed

스크린샷 (선택)

image

@SongMinQQ SongMinQQ self-assigned this Feb 24, 2026
@SongMinQQ SongMinQQ added the enhancement New feature or request label Feb 24, 2026
@SongMinQQ SongMinQQ linked an issue Feb 24, 2026 that may be closed by this pull request
@SongMinQQ SongMinQQ changed the title Feat/generate group 그룹 생성 Feb 24, 2026
@SongMinQQ SongMinQQ changed the title 그룹 생성 여행 그룹 생성 Feb 24, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @SongMinQQ, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

이 풀 리퀘스트는 그룹 생성 기능을 개선하기 위해 UI 컴포넌트의 책임 분리, API 로직의 명확화, 그리고 보안 강화를 목표로 합니다. 사용자 경험을 향상시키고 코드 유지보수성을 높이기 위해 폼 처리 방식과 파일 업로드 메커니즘을 재정비했으며, CSRF 토큰 처리를 도입하여 애플리케이션의 보안 수준을 한 단계 끌어올렸습니다. 또한, 새로운 E2E 테스트를 통해 이러한 변경 사항들이 의도대로 작동하는지 검증합니다.

Highlights

  • 그룹 생성 폼 리팩토링 및 모듈화: 그룹 생성 폼의 책임을 분리하여 GroupGenerateForm을 컨테이너로, GroupGenerateRequiredFields, GroupGenerateOptionalFields, GroupGenerateSubmitBtn을 각각 필수 입력, 선택 입력, 제출 로직을 담당하는 UI 컴포넌트로 재구성했습니다.
  • API 책임 분리 및 파일 업로드 로직 개선: 기존 generateGroup API에 포함되어 있던 파일 업로드 로직을 shared/api/file/uploadFile.ts로 분리했습니다. 이제 generateGroup은 그룹 생성에만 집중하고, 필요한 경우 uploadFile을 호출하여 사전 서명된 URL을 통한 파일 업로드(발급 → PUT → 완료 확인)를 처리합니다.
  • CSRF 토큰 처리 로직 추가: Axios 인터셉터를 사용하여 CSRF 토큰을 요청 헤더에 자동으로 추가하고 응답 헤더에서 읽어 세션 스토리지에 저장하는 로직을 구현하여 보안을 강화했습니다.
  • 폼 상태 모델링 및 상수화: requiredFormoptionalForm으로 폼 상태를 분리 관리하고, 그룹명, 설명, 인원 제한 등 그룹 생성 폼의 제약값을 상수로 정의하여 일관성을 확보했습니다.
  • 재사용 가능한 UI 컴포넌트 도입: 필드 라벨과 글자 수 카운터를 함께 표시하는 FieldLabelWithCounter와 다양한 타입의 라벨링된 폼 필드(LabeledInputField, LabeledTextareaField, LabeledRangeField, LabeledFileSelectButtonField)를 추가하여 UI 일관성과 개발 편의성을 높였습니다.
  • Playwright E2E 테스트 추가: 그룹 생성 및 이미지 업로드 플로우에 대한 Playwright E2E 테스트 시나리오를 추가하여 기능의 안정성을 검증했습니다. 이미지 업로드 성공 및 실패 시 그룹 생성 요청이 올바르게 동작하는지 확인합니다.
Changelog
  • app/group/[id]/reviews/create/page.tsx
    • 리뷰 생성 페이지에서 제목 필드에 FieldLabelWithCounter 컴포넌트를 적용하여 UI 일관성을 확보했습니다.
  • app/group/[id]/schedules/create/page.tsx
    • 일정 생성 페이지에서 일정명, 날짜, 일정 멤버 필드에 FieldLabelWithCounter 컴포넌트를 적용하여 UI 일관성을 확보했습니다.
  • app/group/create/page.tsx
    • 기존의 인라인 그룹 생성 폼 로직을 제거하고 GroupGenerateForm 컴포넌트를 사용하여 페이지의 복잡성을 줄였습니다.
  • features/group/GroupGenerateForm.tsx
    • 그룹 생성 폼의 상태를 관리하고 필수 및 선택 필드 컴포넌트, 제출 버튼을 조합하는 새로운 컨테이너 컴포넌트를 추가했습니다.
  • features/group/model/groupGenerateForm.ts
    • 그룹 생성 폼의 유효성 검사 제약 조건(최대 길이, 최소/최대/기본 인원)을 상수로 정의하고, 폼 상태를 위한 인터페이스를 추가했습니다.
  • features/group/ui/GroupGenerateOptionalFields.tsx
    • 그룹 이미지 업로드와 같은 선택적 필드를 렌더링하고 관리하는 새로운 UI 컴포넌트를 추가했습니다.
  • features/group/ui/GroupGenerateRequiredFields.tsx
    • 그룹명, 설명, 최대 인원, 공개 여부와 같은 필수 필드를 렌더링하고 관리하는 새로운 UI 컴포넌트를 추가했습니다.
  • features/group/ui/GroupGenerateSubmitBtn.tsx
    • 그룹 생성 폼 제출 로직을 담당하는 새로운 UI 컴포넌트를 추가했습니다. 이 컴포넌트는 유효성 검사, 파일 업로드, 그룹 생성 API 호출을 처리합니다.
  • package.json
    • Playwright E2E 테스트 프레임워크를 위한 @playwright/test 의존성을 추가했습니다.
  • pnpm-lock.yaml
    • 새로운 의존성 추가에 따라 pnpm-lock.yaml 파일을 업데이트했습니다.
  • shared/api/common.ts
    • Axios 요청 인터셉터에 CSRF 토큰을 추가하는 로직을 구현하고, 응답 인터셉터에서 CSRF 토큰을 읽어 세션 스토리지에 저장하는 로직을 추가했습니다.
    • 401 에러 발생 시 CSRF 토큰을 초기화하는 로직을 추가했습니다.
  • shared/api/file/uploadFile.ts
    • 사전 서명된 URL을 사용하여 파일을 업로드하고 완료를 확인하는 공통 파일 업로드 API 유틸리티를 추가했습니다.
  • shared/api/group/generateGroup.ts
    • 그룹 생성 API가 uploadFile 유틸리티를 사용하여 썸네일 이미지를 업로드하고, 그 결과를 그룹 생성 요청에 포함하도록 수정했습니다.
  • shared/api/group/types.ts
    • 그룹 생성 요청 데이터 및 응답을 위한 GroupGenerateDataGroupGenerateResponse 인터페이스를 추가했습니다.
  • shared/constants/auth.ts
    • 인증 관련 상수(AUTH_STORAGE_KEYS)에 CSRF 토큰 저장 키를 추가했습니다.
  • shared/constants/paths.ts
    • 파일 업로드 관련 API 경로(FILES.UPLOAD_PRESIGN, FILES.UPLOAD_COMPLETE)를 추가했습니다.
  • shared/lib/csrf-token.ts
    • CSRF 토큰을 읽고, 설정하고, 지우는 유틸리티 함수들을 포함하는 새로운 파일을 추가했습니다.
  • shared/ui/field-label-with-counter.tsx
    • 라벨과 글자 수 카운터를 함께 표시하는 재사용 가능한 UI 컴포넌트를 추가했습니다.
  • shared/ui/labeled-form-fields.tsx
    • 라벨이 있는 입력 필드, 텍스트 영역, 범위 슬라이더, 파일 선택 버튼을 위한 재사용 가능한 UI 컴포넌트들을 추가했습니다.
  • tests/e2e/group-create.spec.js
    • 그룹 생성 및 이미지 업로드 플로우에 대한 Playwright E2E 테스트를 추가했습니다. 성공 및 실패 케이스를 모두 포함합니다.
  • tests/e2e/kakao-login.spec.js
    • 카카오 로그인 E2E 테스트에 CSRF 토큰 처리 로직을 추가하여 로그인 시 CSRF 토큰이 올바르게 저장되는지 확인했습니다.
Activity
  • 그룹 생성 폼의 UI/UX 개선 및 API 책임 분리를 위한 코드 변경이 이루어졌습니다.
  • CSRF 토큰 처리 로직이 추가되어 애플리케이션의 보안이 강화되었습니다.
  • 새로운 Playwright E2E 테스트가 추가되었으며, 그룹 생성 및 이미지 업로드 시나리오가 성공적으로 통과했습니다 (2 passed).
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request successfully refactors the group creation feature, enhancing responsibility separation, maintainability, and testability through a clear component structure and distinct API responsibilities. It also introduces a CSRF protection mechanism. However, a critical security vulnerability has been identified: the CSRF token storage in shared/lib/csrf-token.ts uses a global cache variable, which can cause user-specific tokens to leak between requests during server-side rendering (SSR), leading to session cross-pollination. This implementation should be restricted to the client-side. Please also review the minor improvements suggested in the specific code comments.

const groupData: GroupGenerateData = {
name: trimmedName,
description: trimmedDescription,
memeberLimit: requiredForm.maxMembers,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

GroupGenerateData 인터페이스에서 정의된 memeberLimit에 오타가 있습니다. memberLimit로 수정하는 것이 좋습니다. 이 오타는 shared/api/group/types.ts에서 비롯된 것으로 보이며, 해당 파일에서 수정하면 연관된 다른 파일들의 문제도 해결될 것입니다.

Suggested change
memeberLimit: requiredForm.maxMembers,
memberLimit: requiredForm.maxMembers,

Comment thread shared/api/group/generateGroup.ts Outdated
const requestBody: GroupGenerateRequest = {
name: groupData.name,
description: groupData.description,
memberLimit: groupData.memeberLimit,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

GroupGenerateData 인터페이스에서 정의된 memeberLimit에 오타가 있습니다. memberLimit로 수정하는 것이 좋습니다. 이 오타는 shared/api/group/types.ts에서 비롯된 것으로 보이며, 해당 파일에서 수정하면 연관된 다른 파일들의 문제도 해결될 것입니다.

Suggested change
memberLimit: groupData.memeberLimit,
memberLimit: groupData.memberLimit,

Comment thread shared/api/group/types.ts Outdated
Comment on lines +27 to +29
memeberLimit: number;
groupKind: string;
thumbNailUrl: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

GroupGenerateData 인터페이스에 오타가 있습니다.

  • memeberLimitmemberLimit
  • thumbNailUrlthumbnailUrl
    수정하는 것이 일관성 있고 명확해 보입니다.

이 변경은 아래 파일들에도 연쇄적으로 수정이 필요합니다.

  • features/group/ui/GroupGenerateSubmitBtn.tsx
  • shared/api/group/generateGroup.ts (여기서는 GroupGenerateRequest 인터페이스도 함께 수정해야 합니다)
Suggested change
memeberLimit: number;
groupKind: string;
thumbNailUrl: string;
memberLimit: number;
groupKind: string;
thumbnailUrl: string;

Comment thread shared/lib/csrf-token.ts
Comment on lines +60 to +74
export const setCsrfToken = (token: string | null | undefined): void => {
const normalizedToken = normalizeToken(token);
csrfTokenCache = normalizedToken;

if (typeof window === "undefined") {
return;
}

if (normalizedToken) {
window.sessionStorage.setItem(AUTH_STORAGE_KEYS.CSRF_TOKEN, normalizedToken);
return;
}

window.sessionStorage.removeItem(AUTH_STORAGE_KEYS.CSRF_TOKEN);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

security-medium medium

The csrfTokenCache global variable is used to cache the CSRF token in memory. In a Next.js environment, modules are cached on the server across multiple requests. When setCsrfToken is called during a server-side request (e.g., in a Server Component or during SSR), it updates this global variable. Subsequent requests from different users handled by the same server process will then use the cached CSRF token from the previous user. This leads to CSRF token leakage between users and potential session cross-pollination, which can cause requests to fail or potentially bypass security checks if the token is leaked to the client.

To fix this, ensure that the in-memory cache is only used on the client-side by checking typeof window !== 'undefined' before setting or returning the cached value.

Suggested change
export const setCsrfToken = (token: string | null | undefined): void => {
const normalizedToken = normalizeToken(token);
csrfTokenCache = normalizedToken;
if (typeof window === "undefined") {
return;
}
if (normalizedToken) {
window.sessionStorage.setItem(AUTH_STORAGE_KEYS.CSRF_TOKEN, normalizedToken);
return;
}
window.sessionStorage.removeItem(AUTH_STORAGE_KEYS.CSRF_TOKEN);
};
export const setCsrfToken = (token: string | null | undefined): void => {
const normalizedToken = normalizeToken(token);
if (typeof window === "undefined") {
return;
}
csrfTokenCache = normalizedToken;
if (normalizedToken) {
window.sessionStorage.setItem(AUTH_STORAGE_KEYS.CSRF_TOKEN, normalizedToken);
return;
}
window.sessionStorage.removeItem(AUTH_STORAGE_KEYS.CSRF_TOKEN);
};

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

if (typeof window === "undefined") {
return;
}

현재 반환 전에 여기서 검사중인거 아니야?

htmlFor="review-title"
label={"제목"}
currentLength={title.length}
maxLength={20}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

maxLength20으로 하드코딩되어 있습니다. 그룹 생성 폼에서 하신 것처럼 이 값을 상수로 추출하여 관리하면 유지보수성이 향상될 것입니다. 예를 들어, shared/constants/review.ts 같은 파일에 REVIEW_TITLE_MAX_LENGTH 상수를 정의하고 사용할 수 있습니다.

htmlFor="schedule-name"
label={"일정명"}
currentLength={title.length}
maxLength={20}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

maxLength20으로 하드코딩되어 있습니다. 그룹 생성 폼에서 하신 것처럼 이 값을 상수로 추출하여 관리하면 유지보수성이 향상될 것입니다. 예를 들어, shared/constants/schedule.ts 같은 파일에 SCHEDULE_NAME_MAX_LENGTH 상수를 정의하고 사용할 수 있습니다.

Comment on lines +29 to +40
onGroupNameChange={(groupName) =>
setRequiredForm((prev) => ({ ...prev, groupName }))
}
onDescriptionChange={(description) =>
setRequiredForm((prev) => ({ ...prev, description }))
}
onMaxMembersChange={(maxMembers) =>
setRequiredForm((prev) => ({ ...prev, maxMembers }))
}
onIsPublicChange={(isPublic) =>
setRequiredForm((prev) => ({ ...prev, isPublic }))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

on...Change 핸들러 함수들이 렌더링될 때마다 새로 생성되고 있습니다. 현재는 성능에 큰 영향이 없겠지만, 자식 컴포넌트가 React.memo 등으로 최적화될 경우 불필요한 리렌더링을 유발할 수 있습니다. useCallback을 사용하여 핸들러 함수를 메모이제이션하는 것을 고려해 보세요.

예시:

import { useCallback, ... } from 'react';
...
const onGroupNameChange = useCallback((groupName) => {
  setRequiredForm((prev) => ({ ...prev, groupName }));
}, []);
...
<GroupGenerateRequiredFields
  ...
  onGroupNameChange={onGroupNameChange}
  ...
/>

Comment on lines +77 to +94
<button
type="button"
onClick={() => onIsPublicChange(true)}
className={`text-sm font-semibold transition-colors ${
form.isPublic ? "text-foreground" : "text-muted-foreground"
}`}
>
{"공개"}
</button>
<button
type="button"
onClick={() => onIsPublicChange(false)}
className={`text-sm font-semibold transition-colors ${
!form.isPublic ? "text-foreground" : "text-muted-foreground"
}`}
>
{"비공개"}
</button>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

공개/비공개 버튼의 className을 동적으로 설정하기 위해 템플릿 리터럴을 사용하고 계십니다. 프로젝트의 다른 부분에서는 cn 유틸리티를 일관되게 사용하고 있으므로, 여기서도 cn을 사용하여 코드 스타일의 일관성을 유지하는 것이 좋겠습니다. (import { cn } from '@/shared/lib/utils' 추가 필요)

          <button
            type="button"
            onClick={() => onIsPublicChange(true)}
            className={cn(
              "text-sm font-semibold transition-colors",
              form.isPublic ? "text-foreground" : "text-muted-foreground",
            )}
          >
            {"공개"}
          </button>
          <button
            type="button"
            onClick={() => onIsPublicChange(false)}
            className={cn(
              "text-sm font-semibold transition-colors",
              !form.isPublic ? "text-foreground" : "text-muted-foreground",
            )}
          >
            {"비공개"}
          </button>

@SongMinQQ SongMinQQ merged commit 86e48f5 into develop Feb 24, 2026
@SongMinQQ SongMinQQ deleted the feat/generate-group branch February 24, 2026 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

그룹 생성

1 participant