Conversation
Walkthrough이번 PR은 디자인 시스템 컴포넌트와 대시보드 기능 개선에 집중되었습니다. 일부 컴포넌트에 새로운 선택적 속성과 이벤트 핸들러(onFocus, onBlur 등)가 추가되었으며, 의존성 및 라우트 구성이 업데이트되었습니다. 티켓 옵션 관련 생성 및 관리 기능을 위한 새로운 페이지와 드래그 앤 드롭 인터페이스가 도입되었고, 체크리스트 필터링 기준과 타입 선언도 확장되었습니다. tsconfig 포맷 개선 및 사소한 스타일 오타 수정 등이 포함되어 있습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant U as 사용자
participant T as TicketOptionCreatePage
participant L as 로컬 스토리지
U->>T: 페이지 접근
T->>T: 상태 초기화 및 입력 필드 렌더링
U->>T: 질문 및 옵션 입력
T->>T: 유효성 검증 및 경고 메시지 업데이트
U->>T: 저장 버튼 클릭
T->>L: 데이터 저장
T->>U: 페이지 리디렉션 및 피드백 제공
sequenceDiagram
participant U as 사용자
participant D as TicketOptionPage
participant L as 로컬 스토리지
U->>D: 페이지 접근
D->>D: 상태 초기화 및 드래그 영역 렌더링
U->>D: 항목 드래그 시작 및 이동
D->>D: onDragEnd 이벤트 처리
D->>L: 상태 업데이트 및 데이터 저장
D->>U: 재렌더링으로 업데이트 반영
Suggested labels
Suggested reviewers
Poem
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (13)
src/shared/types/dashboardType.ts (1)
31-31: 메뉴 아이템 추가 및 아이콘 확인 필요새로운 메뉴 항목 '티켓에 추가 옵션 부착'이 추가되었습니다. 코드 끝에 있는 주석("icon, clickedIcon에 들어갈 .svg 알아보기")은 아직 적절한 아이콘을 찾아야 한다는 의미로 보입니다.
적절한 아이콘을 확인한 후에는 주석을 제거하는 것이 좋겠습니다. 현재는 기존 티켓 아이콘을 재사용하고 있는데, 사용자가 구분하기 쉽도록 다른 아이콘을 사용하는 것도 고려해보세요.
- { text: '티켓에 추가 옵션 부착', icon: ticket, clickedIcon: clickedTicket, path: '/dashboard/ticket/option' },// icon, clickedIcon에 들어갈 .svg 알아보기 + { text: '티켓에 추가 옵션 부착', icon: ticket, clickedIcon: clickedTicket, path: '/dashboard/ticket/option' },src/pages/dashboard/ui/ticket/TicketOptionPage.tsx (2)
124-128: children prop 사용 방식 개선이 필요합니다.HorizontalCardButton 컴포넌트에서
labelprop을 통해 자식 요소를 전달하고 있습니다. React에서는 JSX를 통해 직접 자식을 전달하는 것이 더 표준적인 방법입니다.다음과 같이 수정하는 것이 좋습니다:
- label="티켓 새로 생성하기" + + > + 티켓 새로 생성하기단, 이 변경은 HorizontalCardButton 컴포넌트가 children을 지원하는 경우에만 적용해야 합니다.
🧰 Tools
🪛 Biome (1.9.4)
[error] 124-124: Avoid passing children using a prop
The canonical way to pass children in React is to use JSX elements
(lint/correctness/noChildrenProp)
53-106: onDragEnd 함수의 로직이 복잡합니다.드래그 앤 드롭 로직이 잘 구현되어 있지만, 더 모듈화하여 가독성을 높일 수 있을 것 같습니다.
onDragEnd 함수를 더 작은 함수들로 분리하여 가독성을 개선하는 것을 고려해보세요. 예를 들어:
const handleSameColumnDrop = (source, destination, draggableId) => { // 같은 컬럼 내에서의 드래그 로직 }; const handleCrossColumnDrop = (source, destination, draggableId) => { // 다른 컬럼으로의 드래그 로직 }; const onDragEnd = (result: DropResult) => { const { destination, source, draggableId } = result; if (!destination) { return; } if (destination.droppableId === source.droppableId) { handleSameColumnDrop(source, destination, draggableId); return; } handleCrossColumnDrop(source, destination, draggableId); };🧰 Tools
🪛 ESLint
[error] 89-89: 'sourceColumn' is assigned a value but never used.
(@typescript-eslint/no-unused-vars)
design-system/ui/DraggableList.tsx (1)
1-28: 깔끔하게 구현된 드래그 가능한 컴포넌트입니다!
@hello-pangea/dnd라이브러리를 활용하여 드래그 가능한 리스트 아이템을 잘 구현하셨습니다. 코드 구조가 명확하고 각 프로퍼티의 목적이 주석으로 잘 설명되어 있습니다.몇 가지 제안사항:
- 함수 매개변수에 타입 주석 추가
- 컴포넌트에 대한 JSDoc 설명 추가
+/** + * 드래그 앤 드롭 기능을 위한 드래그 가능한 리스트 아이템 컴포넌트 + */ const DraggableList = ({ id, content, index, isDragDisabled = false }: DraggableListProps) => { return ( <Draggable draggableId={id} index={index} isDragDisabled={isDragDisabled}> - {(provided, snapshot) => ( + {(provided: any, snapshot: any) => (design-system/ui/DragArea.tsx (1)
34-40: 고정 높이 설정으로 인한 잠재적 문제현재 드롭 영역에
h-36클래스로 고정 높이를 설정하셨는데, 이는 아이템 수가 많아지면 스크롤이나 오버플로우 문제가 발생할 수 있습니다. 컨텐츠에 따라 높이가 조정되거나, 오버플로우 처리가 필요한지 검토해 보세요.<div ref={provided.innerRef} {...provided.droppableProps} - className={`grid grid-cols-2 gap-4 h-36 ${ + className={`grid grid-cols-2 gap-4 min-h-36 overflow-y-auto ${ !isOptionsArea && snapshot.isDraggingOver ? 'bg-gray-200' : 'bg-white' }`} >src/pages/dashboard/ui/ticket/TicketOptionCreatePage.tsx (8)
1-4: 사용하지 않는 주석 처리된 임포트가 있습니다.2번 줄에 주석 처리된 임포트가 있습니다. 사용할 계획이 없다면 제거하고, 사용할 계획이라면 주석을 해제하세요.
import React from 'react'; -// import { DASHBOARD_ROUTES } from '../../../../app/routes/routes'; import { useNavigate } from 'react-router-dom'; import { useState, useEffect } from 'react';
33-35: 개발용 콘솔 로그를 제거해주세요.프로덕션 환경에 배포하기 전에 개발용 콘솔 로그를 제거하는 것이 좋습니다.
- console.log(`AnswerToggled : ${answerToggled}`); - console.log(`LimitToggled : ${limitToggled}`);
36-50: 불필요한 블록 문이 있습니다.주석을 포함하기 위한 블록 문이 있습니다. 일반 주석으로 대체하는 것이 좋습니다.
- { - /*선택지 삭제*/ - } + // 선택지 삭제 const handleClearOption = (index: number) => { if (options.length === 1) { setWarningMsg('최소 한 개 이상의 선택지를 만들어주세요.'); } else { const updateOptions = options.filter((_, i) => i !== index); setOptions(updateOptions); if (updateOptions.length > 1) { setWarningMsg(''); } } };🧰 Tools
🪛 Biome (1.9.4)
[error] 37-39: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.(lint/complexity/noUselessLoneBlockStatements)
52-62: 여러 불필요한 블록 문을 제거해주세요.주석을 포함하기 위한 블록 문이 여러 곳에 있습니다. 이러한 불필요한 블록 문을 일반 주석으로 대체하세요.
- { - /*선택지 추가*/ - } + // 선택지 추가 const handleAddOption = () => { const updateOptions = [...options, '']; setOptions(updateOptions); if (updateOptions.length > 1) { setWarningMsg(''); } }; - { - /*선택지 입력 업데이트*/ - } + // 선택지 입력 업데이트 const handleInputChange = (index: number, value: string) => { const updateOptions = [...options]; updateOptions[index] = value; setOptions(updateOptions); }; - { - /*ChoiceChip 상태 변화값 감지*/ - } + // ChoiceChip 상태 변화값 감지 useEffect(() => { console.log(`selectedChip 값 : ${selectedChip}`); }, [selectedChip]);Also applies to: 64-71, 73-78
🧰 Tools
🪛 Biome (1.9.4)
[error] 55-56: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.(lint/complexity/noUselessLoneBlockStatements)
76-78: 개발용 콘솔 로그를 제거해주세요.useEffect 내부의 콘솔 로그를 제거하세요.
useEffect(() => { - console.log(`selectedChip 값 : ${selectedChip}`); }, [selectedChip]);🧰 Tools
🪛 Biome (1.9.4)
[error] 77-78: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.(lint/complexity/noUselessLoneBlockStatements)
161-161: 미사용 주석 처리된 코드가 있습니다.주석 처리된
onBlur핸들러가 있습니다. 사용할 계획이 없다면 제거하고, 사용할 계획이라면 주석을 해제하세요.onFocus={() => setFocusedIndex(index)} - // onBlur={() => setFocusedIndex(null)}
212-212: 하드코딩된 경로 대신 상수를 사용하세요.하드코딩된 경로 대신 라우트 상수를 사용하는 것이 좋습니다. 주석 처리된 DASHBOARD_ROUTES 임포트를 활성화하고 해당 상수를 사용하세요.
- <Button label="저장하기" onClick={() => navigate('/dashboard/ticket/option')} className="w-full h-12 rounded-full" /> + <Button label="저장하기" onClick={() => navigate(DASHBOARD_ROUTES.ticketOption)} className="w-full h-12 rounded-full" />
14-23: 폼 상태 관리 개선 제안현재 여러 개의 useState를 사용하여 폼 상태를 관리하고 있습니다. 이는 코드가 복잡해질 수 있습니다. React Hook Form이나 Formik과 같은 폼 라이브러리를 사용하면 상태 관리와 유효성 검사를 더 쉽게 할 수 있습니다.
// 예시: React Hook Form 사용 import { useForm, Controller } from 'react-hook-form'; // 폼 정의 const { control, handleSubmit, watch, setValue, formState: { errors } } = useForm({ defaultValues: { question: '', description: '', responseType: '객관식', required: false, limitQuantity: false, options: ['', '', ''] } }); // submit 핸들러 const onSubmit = (data) => { console.log(data); navigate(DASHBOARD_ROUTES.ticketOption); };이렇게 하면 더 구조화된 방식으로 폼 상태를 관리할 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
yarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (11)
design-system/ui/DragArea.tsx(1 hunks)design-system/ui/DraggableList.tsx(1 hunks)design-system/ui/textFields/DefaultTextField.tsx(3 hunks)package.json(2 hunks)src/app/routes/Router.tsx(3 hunks)src/app/routes/routes.ts(1 hunks)src/features/dashboard/ui/Checklist.tsx(1 hunks)src/pages/dashboard/ui/ticket/TicketOptionCreatePage.tsx(1 hunks)src/pages/dashboard/ui/ticket/TicketOptionPage.tsx(1 hunks)src/shared/types/dashboardType.ts(1 hunks)tsconfig.json(1 hunks)
🧰 Additional context used
🧬 Code Definitions (1)
src/app/routes/Router.tsx (1)
src/app/routes/routes.ts (1)
DASHBOARD_ROUTES(32-45)
🪛 Biome (1.9.4)
src/pages/dashboard/ui/ticket/TicketOptionPage.tsx
[error] 124-124: Avoid passing children using a prop
The canonical way to pass children in React is to use JSX elements
(lint/correctness/noChildrenProp)
src/pages/dashboard/ui/ticket/TicketOptionCreatePage.tsx
[error] 37-39: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
[error] 55-56: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
[error] 67-68: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
[error] 77-78: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
🪛 ESLint
src/pages/dashboard/ui/ticket/TicketOptionPage.tsx
[error] 89-89: 'sourceColumn' is assigned a value but never used.
(@typescript-eslint/no-unused-vars)
design-system/ui/DragArea.tsx
[error] 25-25: 'setData' is defined but never used.
(@typescript-eslint/no-unused-vars)
🔇 Additional comments (20)
package.json (2)
16-16: 새로운 라이브러리 추가 확인
@hello-pangea/dnd라이브러리가 추가되었네요. 이 라이브러리는 PR 목적에 맞게 드래그 앤 드롭 기능을 구현하기 위한 좋은 선택입니다.
61-61:Details
❓ Verification inconclusive
Vite 메이저 버전 업데이트 확인 필요
Vite를 5.x에서 6.x로 메이저 버전 업데이트하셨네요. 메이저 버전 업데이트는 호환성 문제를 일으킬 수 있습니다. 업데이트가 꼭 필요한지, 또는 다른 종속성과의 호환성 문제는 없는지 확인하는 것이 좋겠습니다.
🏁 Script executed:
#!/bin/bash # vite 6.x 버전이 다른 의존성과 호환되는지 확인 grep -r "vite" --include="*.json" --include="*.js" --include="*.ts" . | grep -v "node_modules" | grep -v "package.json" # package-lock.json이 있는 경우 호환성 문제 확인 if [ -f "package-lock.json" ]; then echo "package-lock.json에서 충돌 확인:" grep -A 5 -B 5 "CONFLICT" package-lock.json 2>/dev/null || echo "충돌 없음" fiLength of output: 586
Vite 메이저 버전 업데이트 관련 종속성 호환성 추가 확인 권고
package.json에"vite": "^6.2.2"가 설정되어 있습니다. 해당 버전이 Vite 5.x에서 6.x로 메이저 업데이트되었으므로, breaking change가 발생할 가능성이 있습니다.- 스크립트 실행 결과, package-lock.json에서 명시적인 충돌은 발견되지 않았으며, 프로젝트 내 여러 파일(예:
vite.config.ts, Storybook 설정 파일 등)에서 Vite 관련 참조를 확인할 수 있었습니다.- 다만, 특히
@storybook/react-vite등 Vite를 사용하는 다른 종속성과의 호환성은 빌드 및 통합 테스트를 통해 재확인하는 것이 좋겠습니다.src/app/routes/routes.ts (1)
39-40: 티켓 옵션 관련 새 라우트 추가티켓 옵션 페이지와 티켓 옵션 생성 페이지를 위한 새로운 라우트가 잘 추가되었습니다. 기존 라우트 구조를 따라 적절히 구성되었네요.
src/features/dashboard/ui/Checklist.tsx (1)
7-7: 체크리스트 필터 조건 확인 필요체크리스트에 '티켓에 추가 옵션 부착' 항목이 추가되었습니다. 체크리스트는 "이벤트를 열기 위해 꼭 필요한 정보"를 보여주는 목적을 가지고 있는데, 티켓 추가 옵션이 필수 항목인지 확인이 필요합니다.
티켓 추가 옵션이 이벤트 생성을 위한 필수 과정인지, 아니면 선택적인 기능인지에 따라 체크리스트에 포함하는 것이 적절한지 검토해보세요. 필수가 아니라면 별도의 UI 영역으로 분리하는 것이 사용자 경험 측면에서 더 명확할 수 있습니다.
tsconfig.json (1)
3-10: 가독성 개선이 잘 이루어졌습니다.references 배열을 여러 줄로 나누어 가독성을 향상시켰습니다. 이는 JSON 구성 파일의 유지 관리를 더 쉽게 만들어주는 좋은 변경사항입니다.
src/app/routes/Router.tsx (2)
32-33: 새 페이지 컴포넌트 가져오기 추가가 적절합니다.티켓 옵션 페이지와 티켓 옵션 생성 페이지 컴포넌트를 올바르게 가져왔습니다.
67-67: 새 티켓 옵션 생성 페이지 라우트가 적절히 추가되었습니다.티켓 옵션 생성 페이지를 위한 라우트가 올바르게 추가되었습니다.
design-system/ui/textFields/DefaultTextField.tsx (8)
1-1: FocusEvent 타입 가져오기가 적절합니다.새로운 onFocus 및 onBlur 이벤트 핸들러를 위해 FocusEvent를 올바르게 가져왔습니다.
13-14: 포커스 이벤트 핸들러 추가가 적절합니다.onFocus와 onBlur 이벤트 핸들러 속성을 인터페이스에 추가한 것은 컴포넌트의 상호작용성을 향상시키는 좋은 방법입니다.
18-19: 새로운 스타일링 및 비활성화 옵션이 유용합니다.detailClassName과 disabled 속성을 추가하여 컴포넌트의 유연성을 개선했습니다.
32-33: 이벤트 핸들러 매개변수 추가가 적절합니다.포커스 이벤트 핸들러를 컴포넌트 파라미터에 추가했습니다.
37-38: 새 속성의 기본값 설정이 올바릅니다.detailClassName과 disabled 속성에 대한 기본값이 적절하게 설정되었습니다.
46-46: 세부 레이블에 대한 className 적용이 적절합니다.detailClassName 속성을 사용하여 세부 레이블의 스타일을 사용자 정의할 수 있도록 했습니다.
54-55: 포커스 이벤트 핸들러 적용이 적절합니다.onFocus와 onBlur 이벤트 핸들러를 입력 요소에 올바르게 연결했습니다.
57-57: 비활성화 속성 적용이 올바릅니다.disabled 속성을 입력 요소에 올바르게 적용했습니다.
src/pages/dashboard/ui/ticket/TicketOptionPage.tsx (4)
1-11: 필요한 가져오기가 적절히 구성되었습니다.컴포넌트에 필요한 모든 의존성과 에셋이 올바르게 가져와졌습니다.
12-27: 인터페이스 정의가 명확합니다.
Task,Column,Data인터페이스가 잘 정의되어 있어 타입 안전성이 확보되었습니다.
31-51: 초기 상태 설정이 명확합니다.드래그 앤 드롭 기능을 위한 초기 데이터 상태가 잘 구성되어 있습니다.
108-143: UI 레이아웃이 잘 구성되어 있습니다.컴포넌트의 UI 레이아웃이 명확하게 구성되어 있으며, 사용자에게 적절한 안내 메시지도 포함되어 있습니다.
🧰 Tools
🪛 Biome (1.9.4)
[error] 124-124: Avoid passing children using a prop
The canonical way to pass children in React is to use JSX elements
(lint/correctness/noChildrenProp)
design-system/ui/DragArea.tsx (1)
25-59: 사용하지 않는setData프로퍼티가 있습니다.
setData프로퍼티는 정의되었지만 컴포넌트 내에서 실제로 사용되지 않고 있습니다. 필요하지 않다면 제거하거나, 향후 사용할 계획이라면 주석으로 명시하는 것이 좋겠습니다.const DragArea = ({ data, setData, droppableId }: DragAreaProps) => { const column = data.columns[droppableId]; const isOptionsArea = droppableId === 'options';또한, 31번 줄에 주석 처리된 코드(
{/* <p className="text-sm font-bold p-4">{column.title}</p> */})가 있습니다. 이 코드를 사용할 계획이 있다면 주석을 풀거나, 그렇지 않다면 제거하는 것이 좋습니다.🧰 Tools
🪛 ESLint
[error] 25-25: 'setData' is defined but never used.
(@typescript-eslint/no-unused-vars)
design-system/ui/DragArea.tsx
Outdated
There was a problem hiding this comment.
디자인시스템에 있는 컴포넌트 파일들은 재사용성이 높은 컴포넌트들로 구성했던거에요 그래서 이후에 퍼블리싱 하면서 새로 만드는 컴포넌트들은 각 기능에 맞게 feature, widget, shared 등 폴더에 넣어야해요
DragArea, DraggableList 컴포넌트 모두 드래그앤드롭 기능을 제공하는 UI를 담당하는 파일로 보이는데 features/dashboard/ui에 들어가면 좋을 것 같아요
| {/*인풋 박스 클릭하거나 입력된 내용이 있을 때*/} | ||
| {(focusedIndex === index || option) && ( | ||
| <> | ||
| <div className="block bg-gray-100 rounded-[3px] my-3 p-4"> |
There was a problem hiding this comment.
OptionLimitToggle을 별도 컴포넌트로 분리하면 가독성이나 유지보수에 더욱 좋을 것 같아요:)
d21e58f to
f9d2c7d
Compare
There was a problem hiding this comment.
Actionable comments posted: 30
♻️ Duplicate comments (1)
src/pages/dashboard/ui/ticket/TicketOptionCreatePage.tsx (1)
475-476:⚠️ Potential issue주관식 옵션 구현이 필요합니다
"자유로운 텍스트" 옵션이 선택된 경우의 처리가 미완성 상태입니다.
주석에 "수정해야할 부분"이라고 표시되어 있습니다. 이 기능을 완성해야 합니다.
- {selectedChip === '자유로운 텍스트' && <div></div>} {/*수정해야할 부분*/} + {selectedChip === '자유로운 텍스트' && ( + <div> + {/* 주관식 입력을 위한 UI 구현 */} + <DefaultTextField + placeholder="답변을 입력해주세요." + className="h-12 mb-5" + /> + </div> + )}
🧹 Nitpick comments (68)
src/widgets/event/api/hostChannelList.ts (1)
4-12: 페이지네이션 매개변수를 구성 가능하게 만들면 좋겠습니다.API 호출 구현은 적절하나, 페이지네이션 매개변수(page, size)가 고정되어 있습니다. 이 값들을 함수 매개변수로 받아 기본값을 설정하는 방식으로 변경하면 더 유연하게 사용할 수 있을 것입니다.
-const hostChannelList = async () => { +const hostChannelList = async ({ page = 0, size = 10 } = {}) => { const response = await axiosClient.get<HostChannelListResponse>(`/host-channels`, { params: { - page: 0, - size: 10, + page, + size, }, }); return response.data; };src/shared/ui/KakaoMap.tsx (1)
1-13: 기본 맵 컴포넌트가 잘 구현되었습니다.지정된 위도와 경도를 기반으로 카카오 맵을 렌더링하는 간단한 컴포넌트가 잘 구현되어 있습니다. 그러나 몇 가지 개선이 가능합니다:
- 고정된 높이(300px) 대신 Props를 통해 높이를 동적으로 구성할 수 있게 하는 것이 더 유연할 것 같습니다.
- 마커 스타일링 옵션을 추가하여 더 다양한 사용 사례를 지원할 수 있을 것입니다.
-const KakaoMap = ({ lat, lng }: { lat: number; lng: number }) => { +const KakaoMap = ({ + lat, + lng, + height = '300px', + markerOptions = {} +}: { + lat: number; + lng: number; + height?: string; + markerOptions?: kakao.maps.MarkerOptions; +}) => { return ( - <div className="w-full h-[300px]"> + <div className="w-full" style={{ height }}> <Map center={{ lat, lng }} style={{ width: '100%', height: '100%' }}> - <MapMarker position={{ lat, lng }} /> + <MapMarker position={{ lat, lng }} {...markerOptions} /> </Map> </div> ); };🧰 Tools
🪛 Biome (1.9.4)
[error] 1-1: Do not shadow the global "Map" property.
Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.
(lint/suspicious/noShadowRestrictedNames)
src/app/provider/authStore.ts (1)
1-15: Zustand 스토어 구현이 잘 되었습니다.인증 모달 상태를 관리하기 위한 Zustand 스토어가 명확하고 간결하게 구현되어 있습니다. 코드가 간단하고 목적에 잘 맞게 작성되었습니다.
몇 가지 제안 사항:
- 상태 초기화 또는 리셋 기능을 추가하는 것이 유용할 수 있습니다.
- 모달 열기/닫기 외에 토글 기능을 추가하면 편리할 수 있습니다.
interface AuthStore { isModalOpen: boolean; openModal: () => void; closeModal: () => void; + toggleModal: () => void; + resetState: () => void; } const useAuthStore = create<AuthStore>(set => ({ isModalOpen: false, openModal: () => set({ isModalOpen: true }), closeModal: () => set({ isModalOpen: false }), + toggleModal: () => set((state) => ({ isModalOpen: !state.isModalOpen })), + resetState: () => set({ isModalOpen: false }), }));src/features/event-manage/event-create/hooks/useEventHook.ts (1)
7-13: 이벤트 생성 커스텀 훅이 잘 구현되었습니다.React Query를 활용한 이벤트 생성 커스텀 훅이 효과적으로 구현되어 있습니다. 다만, 몇 가지 개선점을 제안합니다:
- 로딩 상태, 오류 상태 및 성공 상태를 반환하여 컴포넌트에서 쉽게 활용할 수 있도록 합니다.
- 성공 및 오류 콜백을 추가하여 훅 사용 시 더 유연하게 대응할 수 있도록 합니다.
-export const useEventCreation = () => { +export const useEventCreation = ({ + onSuccess, + onError +}: { + onSuccess?: (data: ApiResponse<null>) => void, + onError?: (error: AxiosError) => void +} = {}) => { return useMutation<ApiResponse<null>, AxiosError, CreateEventRequest>({ mutationFn: async (requestBody: CreateEventRequest) => { return await createEvent(requestBody); }, + onSuccess, + onError, }); };src/widgets/event/hook/useHostChannelListHook.tsx (1)
1-15: 파일 확장자 수정 및 캐싱 전략 개선이 필요합니다.이 파일은 JSX를 포함하지 않으므로
.tsx확장자 대신.ts확장자를 사용해야 합니다. 또한, React Query의 캐싱 전략 및 데이터 재검증 옵션을 추가하여 더 효율적인 데이터 관리를 구현할 수 있습니다.const useHostChannelList = () => { const { data, refetch } = useQuery<HostChannelListResponse, AxiosError>({ queryKey: ['hostChannelList'], queryFn: hostChannelList, + staleTime: 5 * 60 * 1000, // 5분 동안 데이터를 신선한 상태로 유지 + cacheTime: 10 * 60 * 1000, // 10분 동안 캐시 유지 + refetchOnWindowFocus: false, // 창 포커스 시 자동 재조회 비활성화 }); return { data, refetch }; };src/features/event-manage/event-create/ui/FileUpload.tsx (1)
3-15: 컴포넌트 인터페이스 개선 필요현재 컴포넌트는 prop이나 콜백을 통해 상위 컴포넌트와 통신할 수 없습니다. 다음 개선이 필요합니다:
- 파일 선택 시 상위 컴포넌트에 알릴 콜백 함수
- 선택된 파일에 접근할 수 있는 인터페이스
- 초기 파일 상태를 받을 수 있는 prop
import FileUploadImage from '../../../../../public/assets/event-manage/creation/FileUpload.svg'; +import { useState, useRef, ChangeEvent, DragEvent } from 'react'; -const FileUpload = () => { +interface FileUploadProps { + onFileSelect?: (file: File | null) => void; + initialFile?: File | null; + className?: string; +} + +const FileUpload = ({ onFileSelect, initialFile = null, className = '' }: FileUploadProps) => { + const [file, setFile] = useState<File | null>(initialFile); + + // 파일 상태가 변경될 때 콜백 호출 + const updateFile = (newFile: File | null) => { + setFile(newFile); + onFileSelect?.(newFile); + }; + return ( - <div className="flex flex-col justify-start gap-1"> + <div className={`flex flex-col justify-start gap-1 ${className}`}> <h1 className="font-bold text-black text-lg">배너 사진 첨부</h1> <h2 className="text-placeholderText text-xs md:text-sm">500kB 이하의 jpg, png 파일만 등록할 수 있습니다.</h2> <div className="flex flex-col items-center justify-center h-44 border border-dashed border-placeholderText bg-gray3 rounded-[10px] mb-4"> <img src={FileUploadImage} alt="파일 업로드" className="w-10 h-10" /> <span className="mt-1 text-black text-sm">이미지를 끌어서 올리거나 클릭해서 업로드 하세요.</span> </div> </div> ); };src/features/event-manage/event-create/model/eventCreation.ts (2)
1-18: 인터페이스 정의 개선 및 문서화 필요
CreateEventRequest인터페이스에 JSDoc 주석과 명확한 타입 정의를 추가하고, 필요한 경우 일부 필드를 선택적(optional)으로 만들면 좋을 것 같습니다.+/** + * 이벤트 생성 요청 인터페이스 + * 호스트 대시보드에서 새 이벤트를 생성할 때 사용됨 + */ export interface CreateEventRequest { + /** 호스트 채널 ID */ hostChannelId: number; + /** 이벤트 제목 */ title: string; + /** 이벤트 시작 날짜 (YYYY-MM-DD 형식) */ - startDate: string; + startDate: string; // 'YYYY-MM-DD' 형식 + /** 이벤트 종료 날짜 (YYYY-MM-DD 형식) */ - endDate: string; + endDate: string; // 'YYYY-MM-DD' 형식 + /** 이벤트 시작 시간 (HH:MM 형식) */ - startTime: string; + startTime: string; // 'HH:MM' 형식 + /** 이벤트 종료 시간 (HH:MM 형식) */ - endTime: string; + endTime: string; // 'HH:MM' 형식 + /** 배너 이미지 URL */ bannerImageUrl: string; + /** 이벤트 설명 */ description: string; + /** 참조 링크 목록 */ - referenceLinks: { address: string; detailAddress: string; title: string; url: string }[]; + referenceLinks: { + /** 링크 주소 */ + address: string; + /** 상세 주소 */ + detailAddress: string; + /** 링크 제목 */ + title: string; + /** URL */ + url: string; + }[]; + /** 온라인/오프라인 유형 */ onlineType: 'ONLINE' | 'OFFLINE'; + /** 이벤트 주소 */ address: string; + /** 이벤트 위치 좌표 */ location: { lat: number; lng: number }; + /** 이벤트 카테고리 */ category: string; + /** 해시태그 목록 */ - hashtags: string[]; + hashtags: string[]; + /** 주최자 이메일 */ organizerEmail: string; + /** 주최자 전화번호 */ organizerPhoneNumber: string; }
1-18: 선택적 필드 정의 및 유효성 검증 로직 추가 고려일부 필드는 선택적일 수 있으며, 유효성 검증 로직을 추가하면 데이터 무결성을 보장하는 데 도움이 됩니다. TypeScript 인터페이스만으로는 런타임 유효성 검증을 직접 구현할 수 없지만, 주석이나 별도의 유효성 검증 함수를 통해 규칙을 명시할 수 있습니다.
export interface CreateEventRequest { hostChannelId: number; title: string; startDate: string; endDate: string; startTime: string; endTime: string; bannerImageUrl: string; description: string; - referenceLinks: { address: string; detailAddress: string; title: string; url: string }[]; + referenceLinks: { address: string; detailAddress?: string; title: string; url: string }[]; onlineType: 'ONLINE' | 'OFFLINE'; - address: string; + address: string; // onlineType이 'OFFLINE'인 경우에만 필수 - location: { lat: number; lng: number }; + location: { lat: number; lng: number }; // onlineType이 'OFFLINE'인 경우에만 필수 category: string; - hashtags: string[]; + hashtags?: string[]; // 선택적 필드 organizerEmail: string; organizerPhoneNumber: string; } +/** + * 이벤트 생성 요청 데이터의 유효성을 검사하는 함수 + * @param data 검증할 이벤트 생성 요청 데이터 + * @returns 유효성 검사 결과 (성공 여부와 오류 메시지) + */ +export const validateEventCreationRequest = (data: CreateEventRequest): { isValid: boolean; errors: string[] } => { + const errors: string[] = []; + + // 필수 필드 검증 + if (!data.title || data.title.trim() === '') { + errors.push('이벤트 제목은 필수입니다.'); + } + + // 날짜 및 시간 형식 검증 + const dateRegex = /^\d{4}-\d{2}-\d{2}$/; + if (!dateRegex.test(data.startDate)) { + errors.push('시작 날짜는 YYYY-MM-DD 형식이어야 합니다.'); + } + if (!dateRegex.test(data.endDate)) { + errors.push('종료 날짜는 YYYY-MM-DD 형식이어야 합니다.'); + } + + // 날짜 유효성 검증 (시작일이 종료일보다 이전이어야 함) + const startDate = new Date(data.startDate); + const endDate = new Date(data.endDate); + if (startDate > endDate) { + errors.push('종료 날짜는 시작 날짜 이후여야 합니다.'); + } + + // onlineType에 따른 필드 검증 + if (data.onlineType === 'OFFLINE') { + if (!data.address || data.address.trim() === '') { + errors.push('오프라인 이벤트는 주소가 필요합니다.'); + } + if (!data.location || typeof data.location.lat !== 'number' || typeof data.location.lng !== 'number') { + errors.push('오프라인 이벤트는 유효한 위치 좌표가 필요합니다.'); + } + } + + // 이메일 형식 검증 + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(data.organizerEmail)) { + errors.push('유효한 이메일 주소를 입력해야 합니다.'); + } + + return { + isValid: errors.length === 0, + errors + }; +};src/features/lib/createFieldMappings.ts (1)
2-4: 추가 엣지 케이스 처리 필요현재 함수는 빈 응답이나 null을 처리하지만 다음과 같은 상황에 대한 추가 처리가 필요합니다:
- 배열의 객체들이 서로 다른 속성을 가질 경우
- 응답에 중첩된 객체나 배열이 포함된 경우
- 객체의 값이
null이나undefined인 경우export const createFieldMappings = (response: Record<string, any>[]) => { if (!response || response.length === 0) return { fieldMap: {}, fieldMapToKorean: {} }; + // 모든 객체의 키를 합쳐서 유니크한 키 목록 생성 + const allKeys = new Set<string>(); + response.forEach(item => { + if (item && typeof item === 'object') { + Object.keys(item).forEach(key => allKeys.add(key)); + } + }); + const sampleResponse = response[0]; const fieldMap: Record<string, string> = {}; const fieldMapToKorean: Record<string, string> = {}; - Object.keys(sampleResponse).forEach((key) => { + // 첫 번째 객체가 아닌 모든 유니크 키를 사용 + Array.from(allKeys).forEach((key) => { fieldMap[key] = key; fieldMapToKorean[key] = key === 'name' ? '이름' : key === 'phone' ? '전화번호' : key === 'email' ? '이메일' : key === 'grade' ? '학년' : key === 'num' ? '학번' : key; }); return { fieldMap, fieldMapToKorean }; };src/features/dashboard/model/ResponseStore.tsx (2)
4-14: ResponseState 인터페이스가 명확하게 정의되었습니다.인터페이스가 상태와 액션을 명확하게 구분하고 있어 좋습니다. 다만,
selectedResponse가 배열 타입인데 네이밍이 단수형인 점이 혼란스러울 수 있습니다.- selectedResponse: responsesData[]; + selectedResponses: responsesData[];관련 메서드도 함께 수정해야 합니다:
- setSelectedResponse: (responseName: string, responseEmail: string) => void; + setSelectedResponses: (responseName: string, responseEmail: string) => void;
22-27: setResponses 메서드에서 견고성 개선이 필요합니다.빈 배열에 대한 처리는 잘 되어 있지만,
response[0]의 첫 번째 또는 두 번째 필드에 대한 가정이 코드를 취약하게 만들 수 있습니다. 특히 첫 번째 필드를 건너뛰고 두 번째 필드(Object.keys(response[0])[1])를 선택하는 로직은 응답 구조가 변경될 경우 문제가 될 수 있습니다.- selectedField: response.length > 0 ? Object.keys(response[0])[1] : '' + selectedField: response.length > 0 && Object.keys(response[0]).length > 0 + ? Object.keys(response[0])[0] // 첫 번째 필드를 기본값으로 설정하거나 + : ''또는 특정 필드명을 명시적으로 선택하는 것이 더 견고할 수 있습니다:
- selectedField: response.length > 0 ? Object.keys(response[0])[1] : '' + selectedField: response.length > 0 && response[0]?.name + ? 'name' // 'name' 필드가 있으면 이를 선택 + : response.length > 0 ? Object.keys(response[0])[0] : ''src/features/event-manage/event-create/hooks/useHostHook.ts (2)
6-12: useHostCreation 훅이 간결하게 구현되었습니다.호스트 생성을 위한 mutation 훅이 잘 구현되어 있습니다. 다만, 오류 처리나 로딩 상태 관리를 위한 추가 옵션이 없습니다.
export const useHostCreation = () => { return useMutation<ApiResponse<null>, Error, HostCreationRequest>({ mutationFn: async (requestBody: HostCreationRequest) => { return await createHost(requestBody); }, + onError: (error) => { + console.error('Failed to create host:', error); + // 사용자에게 오류 피드백을 제공하는 로직을 추가할 수 있습니다 + }, }); };
14-20: useHostDeletion 훅이 간결하게 구현되었습니다.호스트 삭제를 위한 mutation 훅이 잘 구현되어 있습니다. 여기서도 useHostCreation과 마찬가지로 오류 처리나 로딩 상태 관리를 위한 추가 옵션을 고려해볼 수 있습니다.
export const useHostDeletion = () => { return useMutation<ApiResponse<null>, Error, number>({ mutationFn: async (hostChannelId: number) => { return await deleteHost(hostChannelId); }, + onError: (error) => { + console.error('Failed to delete host:', error); + // 사용자에게 오류 피드백을 제공하는 로직을 추가할 수 있습니다 + }, + onSuccess: () => { + // 삭제 후 UI 업데이트 또는 알림을 제공하는 로직을 추가할 수 있습니다 + }, }); };src/widgets/dashboard/ui/ResponsesFilterBar.tsx (1)
8-31: 불필요한 클래스 중복과 공백이 있습니다.ResponsesFilterBar 컴포넌트 구현은 전반적으로 잘 되어 있으나, 몇 가지 개선할 점이 있습니다:
- 13번 줄에 "flex"가 두 번 사용되었습니다.
- 클래스 문자열 끝에 불필요한 공백이 있습니다.
다음과 같이 수정해보세요:
- <div className="flex items-center justify-between text-sm md:text-base py-2 flex gap-20 "> + <div className="flex items-center justify-between text-sm md:text-base py-2 gap-20">src/features/event-manage/event-create/ui/TimePicker.tsx (1)
14-49: UI 컴포넌트 구현은 좋으나 몇 가지 개선이 필요합니다.전반적으로 UI 구현은 잘 되어 있으나, 다음 사항을 개선해야 합니다:
- 유효하지 않은 Tailwind 클래스 "w-15"가 사용되었습니다 (26, 41줄).
- 이벤트 핸들러에서 상태 업데이트 후 부모 컴포넌트에 변경 사항을 알려야 합니다.
- 접근성 개선이 필요합니다.
수정 예시:
<select value={selectedHour} - onChange={e => setSelectedHour(e.target.value)} - className="border rounded-md py-2 md:w-20 w-15 text-sm md:text-base text-center" + onChange={e => { + const newHour = e.target.value; + setSelectedHour(newHour); + handleChange(selectedDate, newHour, selectedMinute); + }} + className="border rounded-md py-2 md:w-20 w-16 text-sm md:text-base text-center" + aria-label="시간 선택" > <select value={selectedMinute} - onChange={e => setSelectedMinute(e.target.value)} - className="border rounded-md py-2 md:w-20 w-15 md:text-base text-sm text-center" + onChange={e => { + const newMinute = e.target.value; + setSelectedMinute(newMinute); + handleChange(selectedDate, selectedHour, newMinute); + }} + className="border rounded-md py-2 md:w-20 w-16 md:text-base text-sm text-center" + aria-label="분 선택" >또한 DatePicker의 onChange 이벤트도 비슷하게 수정해야 합니다:
<DatePicker selected={selectedDate} - onChange={date => setSelectedDate(date)} + onChange={date => { + setSelectedDate(date); + if (date) handleChange(date, selectedHour, selectedMinute); + }} dateFormat="yyyy년 MM월 dd일" + aria-label="날짜 선택" className="border rounded-md py-2 w-32 md:w-40 text-center text-sm md:text-base" />src/features/event-manage/event-create/ui/ShareEventModal.tsx (1)
1-8: 인터페이스 이름에 복수형을 사용하는 것이 컨벤션입니다.컴포넌트 props 인터페이스는 일반적으로 복수형을 사용합니다. 또한 하드코딩된 이미지 경로가 있습니다.
다음과 같이 수정해 보세요:
-interface ShareEventModalProp { +interface ShareEventModalProps { closeModal: () => void; eventName: string; + profileImage?: string; // 선택적 프로필 이미지 추가 }src/features/dashboard/ui/OptionSection.tsx (2)
1-15: 인터페이스 정의는 명확하지만 Response 인터페이스를 외부로 분리하는 것이 좋습니다.Response 인터페이스를 다른 컴포넌트에서도 재사용할 수 있도록 shared 타입으로 분리하는 것이 좋습니다.
다음과 같이 수정해 보세요:
import Checkbox from "../../../../design-system/ui/Checkbox"; -import { Option } from "../../../shared/types/responseType"; +import { Option, Response } from "../../../shared/types/responseType"; -interface Response { - id: string; - name: string; - selectedOptions: { - [key: string]: string; - }; -} interface OptionSectionProps { responses: Response[]; options: Option[]; }그리고
src/shared/types/responseType.ts파일에 Response 인터페이스를 추가하세요:// 파일에 추가할 내용 export interface Response { id: string; name: string; selectedOptions: { [key: string]: string; }; }
17-47: 빈 onChange 핸들러와 복잡한 중첩 맵 함수를 개선해야 합니다.
- 빈 onChange 핸들러는 컴포넌트의 의도를 명확히 하지 않습니다.
- 중첩된 map 함수는 코드 가독성을 떨어뜨립니다.
- 코드 중복이 발생합니다.
다음과 같이 개선해 보세요:
const OptionSection = ({ responses, options }: OptionSectionProps) => { + // 응답이 없을 경우 처리 + if (!responses || responses.length === 0) { + return <div className="text-gray-500">응답이 없습니다.</div>; + } + // 옵션을 렌더링하는 컴포넌트 분리 + const renderOption = (response: Response, option: Option) => { + const selectedChoice = response.selectedOptions[option.optionName]; + + return ( + <div key={option.optionName}> + <p className="block mb-2 text-sm md:text-lg">{option.optionName}</p> + <ul className="space-y-1"> + {option.choices.map((choice) => ( + <li key={choice}> + <Checkbox + label={choice} + checked={selectedChoice === choice} + onChange={() => {/* 읽기 전용 - 선택 불가 */}} + disabled={selectedChoice !== choice} + /> + </li> + ))} + </ul> + </div> + ); + }; return ( <div> {responses.map((response) => ( <div key={response.id}> - {options.map((option) => { - const selectedChoice = response.selectedOptions[option.optionName]; - - return ( - <div key={option.optionName}> - <p className="block mb-2 text-sm md:text-lg">{option.optionName}</p> - <ul className="space-y-1"> - {option.choices.map((choice: string) => ( - <li key={choice}> - <Checkbox - label={choice} - checked={selectedChoice === choice} - onChange={() => {}} - disabled={selectedChoice !== choice} - /> - </li> - ))} - </ul> - </div> - ); - })} + {options.map((option) => renderOption(response, option))} </div> ))} </div> ); };src/features/dashboard/ui/DraggableList.tsx (3)
63-63: 텍스트 길이 제한 개선 필요현재 하드코딩된 6자 제한이 매우 짧아 대부분의 옵션 내용이 잘릴 가능성이 높습니다. 더 긴 제한을 설정하거나 상수로 분리하여 설정 파일에서 관리하는 것이 좋습니다.
- {content.length > 6 ? content.slice(0, 6) + '...' : content} + {content.length > 15 ? content.slice(0, 15) + '...' : content}
73-73: 텍스트 길이 제한 일관성 유지 필요droppableId가 'options'일 때는 6자, 그렇지 않을 때는 8자로 제한이 다르게 설정되어 있습니다. 일관성을 위해 동일한 길이 제한을 사용하거나, 상수로 분리하여 관리하는 것이 좋습니다.
- {content.length > 8 ? content.slice(0, 8) + '...' : content} + {content.length > 15 ? content.slice(0, 15) + '...' : content}
79-79: 삭제 기능에 확인 다이얼로그 추가 권장사용자가 실수로 삭제 버튼을 클릭할 수 있으므로, 삭제 전 확인 다이얼로그를 추가하는 것이 좋습니다.
- <IconButton iconPath={<img className="w-3 h-3" src={DeleteIcon} />} onClick={() => onDelete(id)} size="small" /> + <IconButton + iconPath={<img className="w-3 h-3" src={DeleteIcon} />} + onClick={() => { + if (window.confirm('이 옵션을 삭제하시겠습니까?')) { + onDelete(id); + } + }} + size="small" + />src/shared/types/responseType.ts (2)
12-20: 인터페이스 이름 컨벤션 개선 필요TypeScript 인터페이스 이름은 일반적으로 PascalCase를 사용합니다. 'responsesData'보다는 'ResponsesData'로 변경하는 것이 표준 컨벤션에 맞습니다.
-export interface responsesData { +export interface ResponsesData { id: string; name: string; email: string; phone: string; grade: string; num: string; selectedOptions: { [key: string]: string }; }
17-19: 제한된 값 집합에 Enum 사용 고려grade 필드는 제한된 값 집합(1, 2, 3, 4)을 가지고 있습니다. 이런 경우 string 대신 Enum을 사용하면 타입 안전성을 높일 수 있습니다.
+export enum Grade { + First = "1", + Second = "2", + Third = "3", + Fourth = "4" +} export interface responsesData { id: string; name: string; email: string; phone: string; - grade: string; + grade: Grade; num: string; selectedOptions: { [key: string]: string }; }src/features/event-manage/event-create/ui/TextEditor.tsx (2)
1-1: 미완성 주석 제거 필요개발 중인 TODO 주석이 코드에 남아있습니다. 이러한 주석은 제거하거나 공식 TODO 형식(예:
// TODO: 사진 첨부 기능 구현)으로 변경하는 것이 좋습니다.-// 사진 첨부는 추후에... +// TODO: 사진 첨부 기능 추후 구현 예정
26-61: 컴포넌트 기능 확장 필요TextEditor 컴포넌트가 초기 콘텐츠를 props로 받거나 내부 콘텐츠를 부모 컴포넌트에 노출하는 기능이 없습니다. 이로 인해 재사용성이 제한됩니다.
-const TextEditor = () => { +interface TextEditorProps { + initialContent?: string; + onChange?: (content: string) => void; +} + +const TextEditor = ({ initialContent = '', onChange }: TextEditorProps) => { - const [content, setContent] = useState(''); + const [content, setContent] = useState(initialContent); const handleChange = (value: string) => { - const newText = value.replace(/<\/?[^>]+(>|$)/g, ''); // 태그 제거 - setContent(newText); - console.log(newText); + setContent(value); + if (onChange) { + onChange(value); + } }; // ... 나머지 코드 유지src/features/dashboard/model/TicketOptionStore.tsx (2)
10-16: 인터페이스에 JSDoc 주석 추가 권장인터페이스 및 메서드에 대한 JSDoc 주석이 없어 코드의 목적과 사용법을 이해하기 어렵습니다. 주석을 추가하여 코드 가독성과 유지보수성을 향상시키는 것이 좋습니다.
+/** + * 티켓 옵션 상태 관리를 위한 인터페이스 + */ interface TicketOptionState { + /** 현재 페이지 번호 */ currentPage: number; + /** 페이지 번호 설정 함수 */ setCurrentPage: (page: number) => void; + /** 선택된 옵션 상태 (인덱스별 옵션명과 값 맵핑) */ selectedOptions: { [index: number]: { [key: string]: string | string[] } }; + /** 특정 인덱스의 옵션 값 설정 함수 */ setOption: (index: number, optionName: string, value: string | string[]) => void; }
22-32: 옵션 값 검증 로직 필요setOption 함수에서 옵션 값에 대한 검증이 없습니다. 빈 문자열, null, undefined 등 유효하지 않은 값이 설정될 수 있습니다. 값 검증 로직을 추가하는 것이 좋습니다.
setOption: (index, optionName, value) => { + // 유효하지 않은 값 체크 + if ( + (typeof value === 'string' && value.trim() === '') || + (Array.isArray(value) && value.length === 0) + ) { + console.warn(`유효하지 않은 옵션 값: ${optionName}`); + // 옵션: 유효하지 않은 값 처리 로직 추가 + } + set((state) => { const updatedSelectedOptions = { ...state.selectedOptions }; if (!updatedSelectedOptions[index]) { updatedSelectedOptions[index] = {}; } updatedSelectedOptions[index][optionName] = value; return { selectedOptions: updatedSelectedOptions }; }); },src/features/event-manage/event-create/ui/EventCategory.tsx (2)
31-37: handleSelect에서 eventState 반영 로직
카테고리를 선택할 때 setEventState를 호출하여 eventState에 반영하는 접근이 직관적입니다. 다만 향후 검증 로직이 필요해질 경우, 별도의 훅이나 유틸리티로 분리해 재사용할 수 있으면 좋겠습니다.
41-69: 드롭다운 UI에 대한 접근성 고려
UI 자체는 깔끔하지만, 키보드 접근성(aria 속성 등)을 조금 더 강화하면 좋겠습니다. 선택된 항목 강조 처리는 일관성 있게 잘 구현되었습니다.src/pages/dashboard/ui/ticket/TicketOptionPage.tsx (4)
168-182: localStorage 동기화 타이밍
데이터 변경 시마다 localStorage를 업데이트하는 useEffect 로직으로, 신뢰도가 높습니다. 빈번한 업데이트로 인한 성능 비용이 크지 않은지 추후 고려하면 더욱 좋겠습니다.
179-223: delete 연산자 대신 undefined 할당 고려
현재 코드에서delete newState.newOption;구문을 확인할 수 있습니다. delete 사용은 객체의 구조를 바꿔 성능상 불이익이 있을 수 있으므로,newState.newOption = undefined;와 같은 할당을 고려해 보세요.🧰 Tools
🪛 Biome (1.9.4)
[error] 194-195: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 197-197: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
239-256: children을 prop으로 전달하는 관례에 대한 개선 제안
<IconText ... children="옵션" />방식 대신 JSX children으로 직접 감싸는 편이 React 관례에 더 부합합니다. 예를 들어:-<IconText iconPath={<img src={Option} alt="추가 버튼" />} children="옵션" className="font-bold pl-2" /> +<IconText iconPath={<img src={Option} alt="추가 버튼" />} className="font-bold pl-2"> + 옵션 +</IconText>🧰 Tools
🪛 Biome (1.9.4)
[error] 239-240: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
229-280: 레이아웃 및 드래그 영역 구조
티켓 옵션을 시각적으로 구분하고 드래그 영역을 분리한 점은 사용자가 기능을 쉽게 이해하도록 도와줍니다. 전체 UI가 깔끔하나, 다양한 뷰포트에서 배치를 고려할 때 반응형 체크도 권장됩니다.🧰 Tools
🪛 Biome (1.9.4)
[error] 239-240: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
[error] 258-259: Avoid passing children using a prop
The canonical way to pass children in React is to use JSX elements
(lint/correctness/noChildrenProp)
[error] 275-275: Avoid passing children using a prop
The canonical way to pass children in React is to use JSX elements
(lint/correctness/noChildrenProp)
src/shared/ui/DropDown.tsx (1)
24-49: 접근성 개선 필요드롭다운 컴포넌트가 시각적으로 잘 구현되어 있지만, 키보드 접근성과 ARIA 속성이 누락되어 있습니다.
다음과 같이 접근성을 개선할 수 있습니다:
<div className="relative"> <button onClick={toggleDropdown} + aria-haspopup="listbox" + aria-expanded={open} className="flex justify-between p-2 text-left bg-white border border-placeholderText rounded-[2px] focus:outline-none sm:min-w-40 text-sm" > <span className="flex items-center justify-between w-full"> <span>{selectedValue}</span> <span>▼</span> </span> </button> {open && ( <div + role="listbox" className="absolute top-full left-0 bg-white border border-placeholderText rounded-[2px] z-50 w-full max-w-52"> {options.map((option) => ( <div key={`${option.v1}-${option.v2}`} + role="option" + aria-selected={selectedValue === option.v1} className="p-2 cursor-pointer hover:bg-dropdown" onClick={() => handleSelect(option)} > {option.v1} </div> ))} </div> )} </div>src/features/event-manage/event-create/ui/EventType.tsx (1)
74-85: 오프라인 이벤트 주소 입력에 대한 유효성 검사 추가 필요오프라인 이벤트일 경우 주소 입력이 필수인지 검증하는 로직이 누락되어 있습니다.
다음과 같이 주소 필드에 대한 유효성 검사를 추가하는 것이 좋겠습니다:
const EventType = ({ className }: EventTypeProps) => { const { eventState, setEventState } = useFunnelState(); const [address, setAddress] = useState(''); const [detailAddress, setDetailAddress] = useState(''); + const [addressError, setAddressError] = useState(''); const handleTypeClick = (type: 'ONLINE' | 'OFFLINE') => { setEventState(prev => ({ ...prev, onlineType: type, })); + if (type === 'OFFLINE' && !address) { + setAddressError('오프라인 이벤트는 주소를 입력해야 합니다.'); + } else { + setAddressError(''); + } }; const handleAddressChange = (address: string) => { setAddress(address); + setAddressError(''); setEventState(prev => ({ ...prev, address: `${address} ${detailAddress}`.trim(), })); }; // ... 나머지 코드 ... return ( // ... 기존 코드 ... {eventState.onlineType === 'OFFLINE' && ( <div className="mt-6 space-y-2"> <h1 className="font-bold text-black text-lg">이벤트는 어디서 진행되나요?</h1> <AddressSearch address={address} setAddress={handleAddressChange} onLocationChange={handleLocationChange} onDetailAddressChange={handleDetailAddressChange} /> + {addressError && <p className="text-red-500 text-sm">{addressError}</p>} <KakaoMap lat={eventState.location.lat} lng={eventState.location.lng} /> </div> )} // ... 기존 코드 ... ); };src/features/event-manage/event-create/ui/DatePicker.tsx (1)
56-152: 날짜 선택 범위 유효성 검사 추가 필요시작 날짜가 종료 날짜보다 후일 수 있는 가능성에 대한 유효성 검사가 없습니다.
다음과 같이 시작 날짜와 종료 날짜에 대한 유효성 검사를 추가하는 것이 좋겠습니다:
const EventDatePicker = ({ className, eventState, setEventState, isLabel = false }: DatePickerProps) => { const [startDate, setStartDate] = useState<Date | null>( eventState?.startDate ? new Date(eventState.startDate) : new Date() ); const [endDate, setEndDate] = useState<Date | null>(eventState?.endDate ? new Date(eventState.endDate) : new Date()); const [startTime, setStartTime] = useState<string>(eventState?.startTime || '06:00'); const [endTime, setEndTime] = useState<string>(eventState?.endTime || '23:00'); + const [dateError, setDateError] = useState<string>(''); // ... 기존 코드 ... + const validateDates = (start: Date | null, end: Date | null, startT: string, endT: string) => { + if (!start || !end) return true; + + const startDateTime = new Date(start); + const [startHour, startMinute] = startT.split(':').map(Number); + startDateTime.setHours(startHour, startMinute); + + const endDateTime = new Date(end); + const [endHour, endMinute] = endT.split(':').map(Number); + endDateTime.setHours(endHour, endMinute); + + return endDateTime > startDateTime; + }; useEffect(() => { if (setEventState) { + const isValid = validateDates(startDate, endDate, startTime, endTime); + setDateError(isValid ? '' : '종료 일시는 시작 일시보다 이후여야 합니다.'); + + if (isValid) { setEventState(prev => ({ ...prev, startDate: startDate ? formatDate(startDate) : '', endDate: endDate ? formatDate(endDate) : '', startTime, endTime, })); + } } }, [startDate, endDate, startTime, endTime, setEventState]); return ( <div className={`flex flex-col w-full ${className}`}> + {dateError && <p className="text-red-500 text-sm mb-2">{dateError}</p>} <div className="flex flex-wrap lg:flex-nowrap items-center justify-between gap-2"> {/* 기존 렌더링 코드 */} </div> </div> ); };src/features/payment/ui/TicketOption.tsx (2)
12-22: handleChange 함수 가독성 개선 필요handleChange 함수의 multiple 타입 처리 부분이 다소 복잡합니다. 별도의 함수로 분리하면 가독성이 향상될 것입니다.
다음과 같이 코드를 개선할 수 있습니다:
const TicketOption = ({ options }: TicketOptionProps) => { const { currentPage, selectedOptions, setOption } = useTicketOptionStore(); const currentSelectedOptions = selectedOptions[currentPage] || {}; + const handleMultipleOption = (optionName: string, value: string) => { + const prevValues = (currentSelectedOptions[optionName] as string[]) || []; + const newValues = prevValues.includes(value) + ? prevValues.filter((v) => v !== value) + : [...prevValues, value]; + setOption(currentPage, optionName, newValues); + }; const handleChange = (type: string, optionName: string, value: string) => { if (type === "text" || type === "single") { setOption(currentPage, optionName, value); } else if (type === "multiple") { - const prevValues = (currentSelectedOptions[optionName] as string[]) || []; - const newValues = prevValues.includes(value) - ? prevValues.filter((v) => v !== value) - : [...prevValues, value]; - setOption(currentPage, optionName, newValues); + handleMultipleOption(optionName, value); } }; // ... 나머지 코드 };
71-90: 텍스트 타입 옵션의 choices 배열 개선 필요텍스트 타입 옵션에 빈 choices 배열이 있는데, 이는 불필요해 보입니다. 타입에 따라 choices를 다르게 처리하는 것이 좋겠습니다.
다음과 같이 텍스트 타입의 경우 choices를 선택적으로 만들거나 사용하지 않는 것이 좋겠습니다:
export interface TOption { type: 'text' | 'single' | 'multiple'; optionName: string; required: boolean; - choices: string[] + choices?: string[] // 텍스트 타입의 경우 불필요 } // 수정된 샘플 데이터 export const options: TOption[] = [ { type: 'text', optionName: '성함을 알려주세요.', required: true, - choices: [''] }, { type: 'single', optionName: '티셔츠 사이즈 선택해주세요.', required: true, choices: ['S', 'M', 'L', 'XL'] }, // ... 나머지 코드 ]이에 맞게 TicketOption 컴포넌트 내부의 렌더링 로직도 수정해야 합니다.
src/features/dashboard/ui/SelectedResponseList.tsx (2)
18-27: 읽기 전용 필드 구성이 잘 되어 있음필드 구성이 객체 배열로 정리되어 있어 유지보수성이 좋습니다. 다만 onChange 핸들러가 비어있는 부분에 대한 개선이 필요합니다.
필드가 읽기 전용이라면 다음과 같이 수정하는 것이 좋습니다:
<UnderlineTextField key={value} label={label} value={response[value as keyof Response]} - onChange={() => { }} + disabled={true} placeholder={placeholder} errorMessage={''} className="mb-4" />또는 UnderlineTextField 컴포넌트가 isReadOnly와 같은 prop을 지원한다면 그것을 사용하는 것이 더 명확할 수 있습니다.
48-65: 기본 response 렌더링 로직selectedResponse가 없을 때 기본 response를 표시하는 로직이 잘 구현되어 있습니다. 그러나 두 조건부 렌더링 코드가 거의 동일하여 중복이 발생합니다.
코드 중복을 줄이기 위해 다음과 같이 리팩토링할 수 있습니다:
- {selectedResponse.length > 0 ? ( - selectedResponse.slice(currentIndex, currentIndex + 1).map((response) => ( - <div className="p-4" key={response.id}> - {fields.map(({ label, value, placeholder }) => ( - <UnderlineTextField - key={value} - label={label} - value={response[value as keyof Response]} - onChange={() => { }} - placeholder={placeholder} - errorMessage={''} - className="mb-4" - /> - ))} - <OptionSection responses={[response]} options={options} /> - </div> - )) - ) : ( - response.slice(currentIndex, currentIndex + 1).map((response) => ( - <div className="p-4" key={response.id}> - {fields.map(({ label, value, placeholder }) => ( - <UnderlineTextField - key={value} - label={label} - value={response[value as keyof Response]} - onChange={() => { }} - placeholder={placeholder} - errorMessage={''} - className="mb-4" - /> - ))} - <OptionSection responses={[response]} options={options} /> - </div> - )) - )} + {(selectedResponse.length > 0 ? selectedResponse : response) + .slice(currentIndex, currentIndex + 1) + .map((response) => ( + <div className="p-4" key={response.id}> + {fields.map(({ label, value, placeholder }) => ( + <UnderlineTextField + key={value} + label={label} + value={response[value as keyof Response]} + onChange={() => { }} + placeholder={placeholder} + errorMessage={''} + className="mb-4" + /> + ))} + <OptionSection responses={[response]} options={options} /> + </div> + ))}src/features/dashboard/ui/ResponseFilter.tsx (2)
44-62: 드롭다운 및 레이아웃 구현드롭다운 구현이 잘 되어 있습니다. 다만 선택 로직에서 조건문이 복잡해 보입니다.
드롭다운 선택 로직을 더 명확하게 만들기 위해, 조건문을 분리하거나 주석을 추가하는 것이 좋을 것 같습니다:
onSelect={(selectedName, selectedEmail) => { + // listType에 따라 다른 필드 비교 방식 사용 + // query 타입: 한국어 필드명과 비교 + // 기타 타입: 원래 필드명과 비교 const selectedOption = options.find(opt => (listType === 'query' ? fieldMapToKorean[opt.v1] === selectedName : opt.v1 === selectedName) && opt.v2 === selectedEmail ); if (selectedOption) { setSelectedField(selectedOption.v1, selectedOption.v2); setCurrentIndex(() => 0); } }}
74-75: 페이지 계산 로직 개선 필요페이지 표시 로직에서 불필요한 계산이 있습니다.
Math.floor(currentIndex / 1) + 1및Math.ceil(responsesLength / 1)에서 1로 나누는 것은 값을 변경하지 않으므로 불필요합니다.- <span className='text-sm md:text-base w-10 text-center'>{Math.floor(currentIndex / 1) + 1} / {Math.ceil(responsesLength / 1)}</span> + <span className='text-sm md:text-base w-10 text-center'>{currentIndex + 1} / {responsesLength}</span>src/pages/dashboard/ui/ticket/TicketOptionCreatePage.tsx (2)
293-338: 편집 모드를 위한 초기화 로직편집 모드에서 옵션을 초기화하는 로직이 잘 구현되어 있습니다.
디버깅용 console.log는 프로덕션 코드에서 제거하는 것이 좋습니다:
- console.log('Editing mode:', editOption);🧰 Tools
🪛 Biome (1.9.4)
[error] 296-299: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.(lint/complexity/noUselessLoneBlockStatements)
403-474: 옵션 입력 필드 렌더링객관식 또는 여러개 선택 옵션을 위한 입력 필드 렌더링이 잘 구현되어 있습니다.
OptionLimitToggle 부분을 별도 컴포넌트로 분리하면 가독성과 유지보수성이 향상될 것입니다. 이전 리뷰 코멘트에서도 언급된 내용입니다:
+ const OptionLimitToggle = ({ index, config, onToggle, onQuantityChange, warningMsg2 }) => ( + <div className="block bg-gray-100 rounded-[3px] my-3 p-4"> + <div className="flex items-center justify-between "> + <div className="w-60 md:w-90"> + <p className="text-m font-semibold text-gray-700">선택지에 대한 수량 제한 걸기</p> + <p className="text-gray-400 text-xs"> + 특정 숫자의 사람만 선택하게 하고 싶다면 해당 선택지를 눌러주세요. + </p> + </div> + <ToggleButton + isChecked={config.limitToggled} + onChange={() => onToggle(index)} + /> + </div> + <div className="w-24"> + <DefaultTextField + placeholder="0" + value={config.quantity} + disabled={!config.limitToggled} + onChange={e => onQuantityChange(index, e.target.value)} + className="h-10 !w-24 mt-1" + /> + </div> + {warningMsg2 && <p className="text-red-500 text-xs mt-1">{warningMsg2}</p>} + </div> + ); // 사용 부분 - {(focusedIndex === index || option || getActiveOptionsConfig()[index].limitToggled) && ( - <> - <div className="block bg-gray-100 rounded-[3px] my-3 p-4"> - <div className="flex items-center justify-between "> - <div className="w-60 md:w-90"> - <p className="text-m font-semibold text-gray-700">선택지에 대한 수량 제한 걸기</p> - <p className="text-gray-400 text-xs"> - 특정 숫자의 사람만 선택하게 하고 싶다면 해당 선택지를 눌러주세요. - </p> - </div> - <ToggleButton - isChecked={getActiveOptionsConfig()[index].limitToggled} - onChange={() => handleLimitToggled(index)} - /> - </div> - <div className="w-24"> - <DefaultTextField - placeholder="0" - value={getActiveOptionsConfig()[index].quantity} - disabled={!getActiveOptionsConfig()[index].limitToggled} - onChange={e => handleQuantityChange(index, e.target.value)} - className="h-10 !w-24 mt-1" - /> - </div> - {warningMsg2 && <p className="text-red-500 text-xs mt-1">{warningMsg2}</p>} - </div> - </> - )} + {(focusedIndex === index || option || getActiveOptionsConfig()[index].limitToggled) && ( + <OptionLimitToggle + index={index} + config={getActiveOptionsConfig()[index]} + onToggle={handleLimitToggled} + onQuantityChange={handleQuantityChange} + warningMsg2={warningMsg2} + /> + )}src/features/dashboard/ui/DragArea.tsx (2)
68-74: 조건부 클래스 로직을 더 간결하게 개선할 필요가 있습니다.현재 코드는 삼항 연산자를 중첩하여 사용하고 있어 가독성이 떨어집니다. 조건부 클래스 로직을 더 간결하게 작성할 수 있습니다.
- className={`${ - isOptionsArea - ? 'h-80 grid grid-cols-2 gap-2 grid-flow-row content-start' - : isTicketArea - ? 'h-48 bg-opacity-5 flex flex-col gap-2 overflow-y-auto [&>*]:flex-shrink-0 scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent hover:scrollbar-thumb-gray-300' - : 'h-80 grid grid-cols-2 gap-2 grid-flow-row content-start' - }`} + className={ + isOptionsArea || !isTicketArea + ? 'h-80 grid grid-cols-2 gap-2 grid-flow-row content-start' + : 'h-48 bg-opacity-5 flex flex-col gap-2 overflow-y-auto [&>*]:flex-shrink-0 scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent hover:scrollbar-thumb-gray-300' + }
46-59: handleDelete 함수에 확인 절차 추가를 고려해 보세요.사용자가 실수로 티켓 영역에서 옵션을 삭제할 수 있습니다. 중요한 옵션의 경우, 삭제 전 확인 절차를 추가하는 것이 좋은 UX 관행입니다.
const handleDelete = (id: string) => { if (isTicketArea) { + // 아래 라인은 예시일 뿐, 실제 구현은 프로젝트에 맞는 확인 다이얼로그 컴포넌트를 사용하세요 + if (!window.confirm('정말로 이 옵션을 삭제하시겠습니까?')) { + return; + } setData(prev => ({ ...prev, dragAreas: { ...prev.dragAreas, ticket: { ...prev.dragAreas.ticket, optionIds: prev.dragAreas.ticket.optionIds.filter(optionId => optionId !== id), }, }, })); } };src/features/event-manage/event-create/ui/EventTag.tsx (2)
56-63: MultilineTextField 대신 SinglelineTextField 사용을 고려해 보세요.해시태그는 일반적으로 한 줄 입력이므로, 멀티라인 텍스트 필드보다는 단일 라인 텍스트 필드가 더 적합합니다. 그리고 Enter 키로 태그를 추가하는 방식을 명확하게 하기 위해 placeholder 텍스트를 더 자세히 작성하는 것이 좋습니다.
- <MultilineTextField - value={inputValue} - onChange={handleInputChange} - onKeyDown={handleKeyDown} - placeholder="엔터를 이용해 태그를 입력하세요" - className="w-full h-40" - disabled={hashtags.length >= MAX_TAGS} - /> + <SinglelineTextField + value={inputValue} + onChange={handleInputChange} + onKeyDown={handleKeyDown} + placeholder="태그를 입력하고 엔터 키를 눌러 추가하세요" + className="w-full" + disabled={hashtags.length >= MAX_TAGS} + /> + {hashtags.length >= MAX_TAGS && ( + <p className="text-red-500 text-xs mt-1">최대 태그 수에 도달했습니다</p> + )}
66-75: 해시태그 UI 접근성 개선이 필요합니다.현재 해시태그를 삭제하는 버튼이 'x' 문자만 사용하고 있어 접근성이 좋지 않습니다. 스크린 리더 사용자를 위한 aria-label 속성과 더 명확한 시각적 표시가 필요합니다.
<div className="flex flex-wrap gap-2"> {eventState?.hashtags.map((tag, index) => ( <div key={index} className="inline-flex items-center border border-main bg-dropdown px-3 py-1 rounded-[1px]"> <span className="text-main mr-2">{tag}</span> - <button onClick={() => removeHashtag(tag)} className="text-main focus:outline-none"> - × + <button + onClick={() => removeHashtag(tag)} + className="text-main focus:outline-none hover:text-red-500 transition-colors" + aria-label={`${tag} 태그 삭제`} + > + × </button> </div> ))} </div>src/pages/dashboard/ui/ResponsesManagementPage.tsx (1)
10-10: 좀 더 명확한 타입 정의가 필요합니다.
listType상태는 현재 유니온 타입으로 정의되어 있지만, 이런 타입은 별도의 타입 정의로 분리하면 코드의 가독성과 재사용성이 향상됩니다.+// 파일 상단에 타입 정의 추가 +type ResponseListType = 'summary' | 'query' | 'individual'; const ResponsesManagementPage = () => { - const [listType, setListType] = useState<'summary' | 'query' | 'individual'>('summary'); + const [listType, setListType] = useState<ResponseListType>('summary');src/shared/config/kakaoMap.d.ts (2)
7-11: GeocoderResult 인터페이스에 추가 속성이 필요합니다.현재 GeocoderResult 인터페이스는 최소한의 속성만을 포함하고 있어 상세한 주소 정보를 활용하기 어렵습니다. Kakao 지도 API는 더 많은 정보를 제공합니다.
interface GeocoderResult { address_name: string; y: string; x: string; + address_type: string; + address: { + address_name: string; + region_1depth_name: string; + region_2depth_name: string; + region_3depth_name: string; + h_code: string; + b_code: string; + mountain_yn: string; + main_address_no: string; + sub_address_no: string; + }; + road_address?: { + address_name: string; + region_1depth_name: string; + region_2depth_name: string; + region_3depth_name: string; + road_name: string; + underground_yn: string; + main_building_no: string; + sub_building_no: string; + building_name: string; + zone_no: string; + }; }
4-4: Geocoder 클래스에 추가 메소드 정의가 필요합니다.Kakao 지도 API의 Geocoder 클래스는 addressSearch 외에도 다른 유용한 메서드를 제공합니다. 특히 coord2Address와 같은 메서드는 지도에서 좌표를 주소로 변환할 때 필요합니다.
class Geocoder { addressSearch(address: string, callback: (result: GeocoderResult[], status: Status) => void): void; + coord2Address(x: number, y: number, callback: (result: Array<{ + address: { + address_name: string; + region_1depth_name: string; + region_2depth_name: string; + region_3depth_name: string; + mountain_yn: string; + main_address_no: string; + sub_address_no: string; + }; + road_address: { + address_name: string; + region_1depth_name: string; + region_2depth_name: string; + road_name: string; + underground_yn: string; + main_building_no: string; + sub_building_no: string; + building_name: string; + zone_no: string; + } | null; + }>, status: Status) => void): void; }src/shared/ui/backgrounds/TicketOptionLayout.tsx (4)
26-33: 불필요한 블록 문 제거 필요handleNextPage 함수 내의 중괄호 블록들이 불필요합니다. 각 블록이 변수 선언이나 다른 블록 레벨 선언 없이 단독으로 사용되고 있어 제거해도 됩니다.
다음과 같이 수정하는 것이 좋습니다:
const handleNextPage = () => { if (isLastPage) { - {/* 데이터 전송 추가 */} + /* 데이터 전송 추가 */ } else { setCurrentPage(currentPage + 1); - {/* 데이터 전송 추가 */} + /* 데이터 전송 추가 */ } };🧰 Tools
🪛 Biome (1.9.4)
[error] 30-31: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.(lint/complexity/noUselessLoneBlockStatements)
28-28: TODO 주석 구현 필요"데이터 전송 추가" TODO 주석이 있습니다. 이 부분에 실제 데이터 전송 로직을 구현해야 합니다.
마지막 페이지에서 사용자가 선택한 옵션 데이터를 서버로 전송하는 로직이 필요합니다. 필요하시면 해당 부분 구현에 도움을 드릴 수 있습니다.
68-68: 티켓 설명 텍스트 수정 필요현재 티켓 설명 텍스트가 반복적인 더미 텍스트로 되어 있습니다. 실제 사용 시에는 의미 있는 설명으로 대체되어야 합니다.
실제 티켓에 대한 설명을 담당자로부터 받아 적용하거나, 동적으로 데이터를 받아와 표시하는 방식으로 구현하세요.
46-55: 접근성 개선 필요현재 페이지 인디케이터가 이미지만으로 구현되어 있어 스크린 리더 사용자에게 접근하기 어려울 수 있습니다.
이미지에 적절한 alt 텍스트를 추가하여 접근성을 개선하세요:
<img key={index} src={index + 1 === currentPage ? active : inactive} className="object-contain" + alt={index + 1 === currentPage ? `현재 페이지 ${index + 1}` : `페이지 ${index + 1}`} />src/features/event-manage/event-create/hooks/useFunnelHook.tsx (2)
14-40: steps 배열 재사용성 개선 필요현재 steps 배열이 훅 내부에 하드코딩되어 있어 재사용성이 제한됩니다. 이 훅을 다른 퍼널 프로세스에 사용하기 어렵습니다.
steps 배열을 매개변수로 받아 더 유연하게 사용할 수 있도록 개선하세요:
- export const useFunnel = (defaultStep: number) => { + export const useFunnel = (defaultStep: number, customSteps?: string[]) => { const [step, setStep] = useState(defaultStep); - const steps = [ + const defaultSteps = [ 'HostSelection', 'HostCreation', 'EventTitle', 'EventPeriod', 'EventOrganizerInfo', 'EventInfo', 'EventType', 'EventTag', ]; + + const steps = customSteps || defaultSteps;이렇게 하면 훅을 사용할 때 커스텀 단계를 제공할 수 있게 됩니다.
14-39: setStep 함수에 타입 안전성 추가 필요현재
setStep함수의 반환 타입이 명시되어 있지 않고, 유효하지 않은 step 인덱스가 전달될 경우에 대한 검증이 없습니다.step 인덱스의 유효성을 검사하는 로직을 추가하세요:
const useFunnel = (defaultStep: number) => { const [step, setStep] = useState(defaultStep); const steps = [ 'HostSelection', 'HostCreation', 'EventTitle', 'EventPeriod', 'EventOrganizerInfo', 'EventInfo', 'EventType', 'EventTag', ]; + const safeSetStep = (newStep: number) => { + if (newStep < 0 || newStep >= steps.length) { + console.error(`Step index ${newStep} is out of bounds (0-${steps.length - 1})`); + return; + } + setStep(newStep); + }; // ... (나머지 코드) - return { Funnel, Step, setStep, currentStep: step, steps }; + return { Funnel, Step, setStep: safeSetStep, currentStep: step, steps }; };이렇게 하면 유효하지 않은 step 인덱스가 전달되었을 때 오류 메시지를 출력하고 상태 변경을 방지할 수 있습니다.
src/features/dashboard/ui/ResponsesList.tsx (3)
15-22: 옵셔널 체이닝 사용 권장현재 코드에서는 response 배열의 존재 여부를 체크한 후 첫 번째 요소에 접근하고 있는데, 이는 옵셔널 체이닝을 사용하여 더 간결하게 작성할 수 있습니다.
다음과 같이 옵셔널 체이닝을 사용하여 코드를 간결하게 만들 수 있습니다:
- const queryOptions = response && response[0] - ? Object.keys(response[0]) - .filter(key => key !== 'id' && key !== 'selectedOptions') - .map(key => ({ - v1: fieldMap[key] || key, - v2: "" - })) - : []; + const queryOptions = response?.[0] + ? Object.keys(response[0]) + .filter(key => key !== 'id' && key !== 'selectedOptions') + .map(key => ({ + v1: fieldMap[key] || key, + v2: "" + })) + : [];🧰 Tools
🪛 Biome (1.9.4)
[error] 15-15: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
28-51: 데이터 없을 때의 메시지 개선 필요현재 응답이 없을 때 단순히 "응답이 없습니다"라는 메시지만 표시되고 있습니다. 사용자 경험을 개선하기 위해 더 풍부한 안내 메시지와 시각적 표현이 필요합니다.
빈 상태에 대한 UI를 개선하세요:
{response.length === 0 ? ( - <p>응답이 없습니다.</p> + <div className="flex flex-col items-center justify-center py-4"> + <svg className="w-12 h-12 text-gray-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> + </svg> + <p className="text-gray-500">아직 응답이 없습니다.</p> + <p className="text-sm text-gray-400 mt-1">응답이 들어오면 이곳에 표시됩니다.</p> + </div> ) : (
92-95: responsesLength 계산 로직 개선 필요selectedResponse가 비어있을 때 전체 response 길이를 사용하는 로직이 복잡하게 표현되어 있습니다. 가독성을 높이기 위해 개선이 필요합니다.
조건부 연산자 대신 좀 더 직관적인 로직으로 개선하세요:
- responsesLength={selectedResponse.length > 0 ? selectedResponse.length : response.length} + responsesLength={selectedResponse.length || response.length}이렇게 하면 selectedResponse.length가 0이면 response.length를 사용하게 됩니다.
src/shared/ui/AddressSearch.tsx (2)
76-81: 접근성 개선 필요닫기 버튼이 이미지만으로 구현되어 있어 스크린 리더 사용자에게 명확한 정보를 제공하지 않습니다. 또한 하드코딩된 이미지 URL은 유지보수성에 문제가 될 수 있습니다.
이미지 대신 접근성이 향상된 아이콘 또는 버튼을 사용하세요:
- <img - src="//t1.daumcdn.net/postcode/resource/images/close.png" - alt="닫기" - className="absolute top-0 right-0 cursor-pointer" - onClick={() => setIsModalOpen(false)} - /> + <button + className="absolute top-0 right-0 p-2 text-gray-600 hover:text-gray-900" + onClick={() => setIsModalOpen(false)} + aria-label="닫기" + > + <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> + <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> + </svg> + </button>이렇게 하면 이미지 URL에 의존하지 않고, 더 접근성이 좋은 방식으로 닫기 버튼을 구현할 수 있습니다.
70-85: 모달 컴포넌트 분리 검토 필요주소 검색 모달이 컴포넌트 내에 인라인으로 구현되어 있어 코드가 복잡해 보입니다. 이 부분을 별도의 컴포넌트로 분리하면 재사용성과 가독성이 향상될 수 있습니다.
모달 부분을 별도의 컴포넌트로 분리하는 것을 고려하세요:
// AddressSearchModal.tsx interface AddressSearchModalProps { isOpen: boolean; onClose: () => void; onComplete: (data: Address) => void; postcodeKey: number; } const AddressSearchModal = ({ isOpen, onClose, onComplete, postcodeKey }: AddressSearchModalProps) => { if (!isOpen) return null; const closeModalOnBackgroundClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { onClose(); } }; return ( <div className="fixed inset-0 flex items-center justify-center w-full max-w-lg h-full mx-auto bg-black bg-opacity-50 z-50" onClick={closeModalOnBackgroundClick} > <div className="relative bg-white rounded-lg p-5"> <button className="absolute top-0 right-0 p-2 text-gray-600 hover:text-gray-900" onClick={onClose} aria-label="닫기" > <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" /> </svg> </button> <DaumPostcode key={postcodeKey} onComplete={onComplete} /> </div> </div> ); };그런 다음 AddressSearch 컴포넌트에서 이 모달 컴포넌트를 사용할 수 있습니다.
src/features/event-manage/event-create/ui/EventFunnel.tsx (3)
1-14: 상대 경로 임포트 개선 필요각 컴포넌트와 훅을 가져오는 임포트문에서
../../../../와 같이 깊은 상대 경로를 사용하고 있습니다. 이는 코드 가독성을 저하시키고, 파일 구조 변경 시 유지 보수를 어렵게 만들 수 있습니다.
tsconfig.json에 path alias를 설정하여 더 명확한 임포트 경로를 사용하는 것을 권장합니다. 예:// tsconfig.json에 다음 설정 추가 { "compilerOptions": { + "baseUrl": ".", + "paths": { + "@pages/*": ["src/pages/*"], + "@features/*": ["src/features/*"], + "@shared/*": ["src/shared/*"] + } } } // 임포트 개선 예시 -import HostSelectionPage from '../../../../pages/event-manage/ui/HostSelectionPage'; +import HostSelectionPage from '@pages/event-manage/ui/HostSelectionPage';
46-122: 단계 간 네비게이션 로직 일관성 부족각 단계 간 이동 로직이 일관되지 않습니다. 일부 단계는
currentStep + 1, 일부는currentStep + 2를 사용하며, 하드코딩된 문자열 변환(String(currentStep + 1))은 오류 발생 가능성이 높습니다.단계 간 이동 로직을 함수나 상수로 추출하고, 일관된 방식으로 적용하는 것이 좋습니다:
+// 단계 이름과 인덱스 매핑 +const STEP_MAP = { + [StepNames.HostSelection]: 0, + [StepNames.HostCreation]: 1, + [StepNames.EventTitle]: 2, + // ... 나머지 단계들 +}; + +// 다음 단계 이름 가져오기 +const getNextStepName = (currentStepName) => { + const currentIndex = STEP_MAP[currentStepName]; + const steps = Object.keys(STEP_MAP); + return steps[currentIndex + 1] || steps[currentIndex]; // 다음 단계 없으면 현재 단계 유지 +}; // 사용 예시 <EventRegisterLayout title="이벤트를 호스팅할 채널을 선택해주세요" - onNext={() => handleNext(String(currentStep + 2))} + onNext={() => handleNext(getNextStepName(StepNames.HostSelection))} onPrev={() => navigate(-1)} requireValidation={true} >
114-121: 마지막 단계 타이틀 누락다른 모든 단계는
title속성을 가지고 있지만, 마지막EventTagPage단계에는 타이틀이 누락되어 있습니다. 일관성을 위해 추가하는 것이 좋습니다.<EventRegisterLayout + title="이벤트 태그를 선택해주세요" onNext={() => handleNext(String(currentStep + 1))} onPrev={() => onPrev(String(currentStep - 1))} > <EventTagPage /> </EventRegisterLayout>src/shared/lib/formValidation.ts (3)
6-9: 중복된 이메일 유효성 검사현재 이메일 필드에
.email()메소드와 정규식 패턴 둘 다 사용하고 있어 중복 검증이 발생합니다. 이는 불필요한 검증 로직을 추가하여 성능에 영향을 줄 수 있습니다.둘 중 하나의 방식만 선택하여 사용하세요. Zod의 기본 이메일 검증은 대부분의 경우 충분합니다:
export const formSchema = z.object({ name: z.string().min(2, '이름은 최소 두 글자 이상이어야 합니다.').max(10, '이름은 최대 10자까지 가능합니다.'), email: z .string() .email('올바른 이메일 형식이어야 합니다.') - .regex(/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/, '올바른 이메일 형식이어야 합니다.'), phone: z.string().regex(/^[0-9]{10,11}$/, '연락처는 10~11자리 숫자여야 합니다.'), });
16-20: 채널 설명 에러 메시지와 검증 로직 불일치채널 설명(channelDescription)의 최소 길이는 5로 설정되어 있지만, 에러 메시지는 "최소 두 글자 이상"이라고 표시됩니다. 이는 사용자에게 혼란을 줄 수 있습니다.
에러 메시지를 실제 검증 로직과 일치하도록 수정하세요:
export const hostCreationSchema = z.object({ hostChannelName: z.string().min(2, '채널 이름은 최소 두 글자 이상이어야 합니다.'), hostEmail: z.string().email('올바른 이메일 형식이어야 합니다.'), - channelDescription: z.string().min(5, '채널 설명은 최소 두 글자 이상이어야 합니다.'), + channelDescription: z.string().min(5, '채널 설명은 최소 다섯 글자 이상이어야 합니다.'), });
10-10: 제한적인 전화번호 검증 패턴현재 전화번호 검증은 숫자만 허용하고 있어 사용자가 하이픈(-)을 포함하여 입력할 경우 유효성 검사에 실패합니다. 이는 사용자 경험을 저하시킬 수 있습니다.
입력 시 하이픈을 허용하고 내부적으로 처리하거나, 사용자에게 더 명확한 안내를 제공하는 방식으로 개선하세요:
- phone: z.string().regex(/^[0-9]{10,11}$/, '연락처는 10~11자리 숫자여야 합니다.'), + phone: z + .string() + .regex(/^[0-9]{10,11}$/, '연락처는 10~11자리 숫자만 입력해주세요. (예: 01012345678)') + .transform(val => val.replace(/-/g, '')), // 입력된 하이픈 제거또는 하이픈을 허용하는 정규식 패턴을 사용할 수도 있습니다:
- phone: z.string().regex(/^[0-9]{10,11}$/, '연락처는 10~11자리 숫자여야 합니다.'), + phone: z.string().regex(/^[0-9]{3}-?[0-9]{3,4}-?[0-9]{4}$/, '유효한 전화번호 형식이 아닙니다. (예: 010-1234-5678 또는 01012345678)'),
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (10)
public/assets/dashboard/ticket/AddButton2.svgis excluded by!**/*.svgpublic/assets/dashboard/ticket/ModifyPencilIcon.svgis excluded by!**/*.svgpublic/assets/dashboard/ticket/Option.svgis excluded by!**/*.svgpublic/assets/dashboard/ticket/deleteIcon.svgis excluded by!**/*.svgpublic/assets/dashboard/ticket/option(black).svgis excluded by!**/*.svgpublic/assets/dashboard/ticket/option(pink).svgis excluded by!**/*.svgpublic/assets/payment/Active.svgis excluded by!**/*.svgpublic/assets/payment/Inactive.svgis excluded by!**/*.svgyarn 2.lockis excluded by!**/*.lockyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (49)
design-system/ui/ChoiceChip.tsx(2 hunks)design-system/ui/buttons/IconButton.tsx(2 hunks)design-system/ui/buttons/TertiaryButton.tsx(1 hunks)design-system/ui/texts/IconText.tsx(1 hunks)src/app/provider/authStore.ts(1 hunks)src/app/routes/Router.tsx(3 hunks)src/features/dashboard/model/ResponseStore.tsx(1 hunks)src/features/dashboard/model/TicketOptionStore.tsx(1 hunks)src/features/dashboard/ui/DragArea.tsx(1 hunks)src/features/dashboard/ui/DraggableList.tsx(1 hunks)src/features/dashboard/ui/OptionSection.tsx(1 hunks)src/features/dashboard/ui/ResponseFilter.tsx(1 hunks)src/features/dashboard/ui/ResponsesList.tsx(1 hunks)src/features/dashboard/ui/SelectedResponseList.tsx(1 hunks)src/features/event-manage/event-create/api/event.ts(1 hunks)src/features/event-manage/event-create/api/host.ts(1 hunks)src/features/event-manage/event-create/hooks/useEventHook.ts(1 hunks)src/features/event-manage/event-create/hooks/useFunnelHook.tsx(1 hunks)src/features/event-manage/event-create/hooks/useHostHook.ts(1 hunks)src/features/event-manage/event-create/model/FunnelContext.tsx(1 hunks)src/features/event-manage/event-create/model/eventCreation.ts(1 hunks)src/features/event-manage/event-create/model/hostCreation.ts(1 hunks)src/features/event-manage/event-create/ui/DatePicker.tsx(1 hunks)src/features/event-manage/event-create/ui/EventCategory.tsx(1 hunks)src/features/event-manage/event-create/ui/EventFunnel.tsx(1 hunks)src/features/event-manage/event-create/ui/EventTag.tsx(1 hunks)src/features/event-manage/event-create/ui/EventType.tsx(1 hunks)src/features/event-manage/event-create/ui/FileUpload.tsx(1 hunks)src/features/event-manage/event-create/ui/LinkInput.tsx(1 hunks)src/features/event-manage/event-create/ui/ShareEventModal.tsx(1 hunks)src/features/event-manage/event-create/ui/TextEditor.tsx(1 hunks)src/features/event-manage/event-create/ui/TimePicker.tsx(1 hunks)src/features/lib/createFieldMappings.ts(1 hunks)src/features/payment/ui/TicketOption.tsx(1 hunks)src/pages/dashboard/ui/ResponsesManagementPage.tsx(1 hunks)src/pages/dashboard/ui/ticket/TicketOptionCreatePage.tsx(1 hunks)src/pages/dashboard/ui/ticket/TicketOptionPage.tsx(1 hunks)src/shared/config/kakaoMap.d.ts(1 hunks)src/shared/lib/formValidation.ts(1 hunks)src/shared/types/dashboardType.ts(2 hunks)src/shared/types/responseType.ts(1 hunks)src/shared/ui/AddressSearch.tsx(1 hunks)src/shared/ui/DropDown.tsx(1 hunks)src/shared/ui/KakaoMap.tsx(1 hunks)src/shared/ui/backgrounds/TicketOptionLayout.tsx(1 hunks)src/widgets/dashboard/ui/ResponsesFilterBar.tsx(1 hunks)src/widgets/event/api/hostChannelList.ts(1 hunks)src/widgets/event/hook/useHostChannelListHook.tsx(1 hunks)src/widgets/event/model/hostChannelList.ts(1 hunks)
✅ Files skipped from review due to trivial changes (2)
- design-system/ui/buttons/TertiaryButton.tsx
- design-system/ui/texts/IconText.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- src/shared/types/dashboardType.ts
- src/app/routes/Router.tsx
🧰 Additional context used
🧬 Code Definitions (20)
src/features/event-manage/event-create/hooks/useEventHook.ts (2)
src/features/event-manage/event-create/model/eventCreation.ts (1)
CreateEventRequest(1-18)src/features/event-manage/event-create/api/event.ts (1)
createEvent(4-7)
src/features/event-manage/event-create/api/event.ts (1)
src/features/event-manage/event-create/model/eventCreation.ts (1)
CreateEventRequest(1-18)
src/widgets/event/hook/useHostChannelListHook.tsx (1)
src/widgets/event/model/hostChannelList.ts (1)
HostChannelListResponse(7-9)
src/widgets/event/api/hostChannelList.ts (1)
src/widgets/event/model/hostChannelList.ts (1)
HostChannelListResponse(7-9)
src/features/event-manage/event-create/ui/EventCategory.tsx (1)
src/features/event-manage/event-create/model/FunnelContext.tsx (1)
FunnelState(5-11)
src/shared/types/responseType.ts (1)
src/features/payment/ui/TicketOption.tsx (1)
options(71-90)
src/features/event-manage/event-create/hooks/useHostHook.ts (2)
src/features/event-manage/event-create/model/hostCreation.ts (1)
HostCreationRequest(1-6)src/features/event-manage/event-create/api/host.ts (2)
createHost(4-7)deleteHost(9-12)
src/features/dashboard/ui/SelectedResponseList.tsx (1)
src/features/dashboard/model/ResponseStore.tsx (1)
useResponseStore(16-47)
src/pages/dashboard/ui/ResponsesManagementPage.tsx (1)
src/features/dashboard/model/ResponseStore.tsx (1)
useResponseStore(16-47)
src/features/event-manage/event-create/model/FunnelContext.tsx (2)
src/features/event-manage/event-create/model/hostCreation.ts (1)
HostCreationRequest(1-6)src/features/event-manage/event-create/model/eventCreation.ts (1)
CreateEventRequest(1-18)
src/features/event-manage/event-create/ui/EventTag.tsx (1)
src/features/event-manage/event-create/model/FunnelContext.tsx (1)
FunnelState(5-11)
src/features/event-manage/event-create/ui/EventType.tsx (2)
src/features/event-manage/event-create/model/FunnelContext.tsx (1)
useFunnelState(53-59)src/shared/ui/AddressSearch.tsx (1)
AddressSearch(12-88)
src/features/event-manage/event-create/api/host.ts (1)
src/features/event-manage/event-create/model/hostCreation.ts (1)
HostCreationRequest(1-6)
src/shared/ui/backgrounds/TicketOptionLayout.tsx (1)
src/features/dashboard/model/TicketOptionStore.tsx (1)
useTicketOptionStore(18-33)
src/features/event-manage/event-create/ui/EventFunnel.tsx (5)
src/features/event-manage/event-create/model/FunnelContext.tsx (1)
useFunnelState(53-59)src/features/event-manage/event-create/api/event.ts (1)
createEvent(4-7)src/features/event-manage/event-create/hooks/useEventHook.ts (1)
useEventCreation(7-13)src/features/event-manage/event-create/api/host.ts (1)
createHost(4-7)src/features/event-manage/event-create/hooks/useHostHook.ts (1)
useHostCreation(6-12)
src/features/event-manage/event-create/ui/LinkInput.tsx (1)
src/features/event-manage/event-create/model/FunnelContext.tsx (1)
FunnelState(5-11)
src/pages/dashboard/ui/ticket/TicketOptionPage.tsx (1)
src/shared/types/responseType.ts (1)
Option(1-4)
src/features/payment/ui/TicketOption.tsx (1)
src/shared/types/responseType.ts (1)
options(5-10)
src/features/dashboard/ui/ResponsesList.tsx (1)
src/features/dashboard/model/ResponseStore.tsx (1)
useResponseStore(16-47)
src/features/event-manage/event-create/ui/DatePicker.tsx (1)
src/features/event-manage/event-create/model/FunnelContext.tsx (1)
FunnelState(5-11)
🪛 Biome (1.9.4)
src/shared/ui/KakaoMap.tsx
[error] 1-1: Do not shadow the global "Map" property.
Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.
(lint/suspicious/noShadowRestrictedNames)
src/shared/ui/backgrounds/TicketOptionLayout.tsx
[error] 30-31: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
[error] 35-36: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
src/pages/dashboard/ui/ticket/TicketOptionCreatePage.tsx
[error] 139-139: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
[error] 166-168: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
[error] 185-186: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
[error] 280-282: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
[error] 296-299: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
src/pages/dashboard/ui/ticket/TicketOptionPage.tsx
[error] 194-195: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 197-197: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
[error] 239-240: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
[error] 258-259: Avoid passing children using a prop
The canonical way to pass children in React is to use JSX elements
(lint/correctness/noChildrenProp)
[error] 275-275: Avoid passing children using a prop
The canonical way to pass children in React is to use JSX elements
(lint/correctness/noChildrenProp)
src/features/dashboard/ui/ResponsesList.tsx
[error] 15-15: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
🪛 ESLint
src/features/dashboard/ui/DragArea.tsx
[error] 37-37: 'answerToggled' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 38-38: 'responseFormat' is defined but never used.
(@typescript-eslint/no-unused-vars)
src/features/lib/createFieldMappings.ts
[error] 1-1: Unexpected any. Specify a different type.
(@typescript-eslint/no-explicit-any)
🔇 Additional comments (72)
src/features/event-manage/event-create/api/event.ts (1)
1-7: API 클라이언트 구현이 잘 되었습니다.이벤트 생성 API 함수가 적절히 구현되었습니다. axiosClient를 사용한 비동기 호출 패턴이 깔끔하고, 타입 정의도 명확합니다.
src/widgets/event/model/hostChannelList.ts (1)
1-9: 인터페이스 정의가 명확합니다.호스트 채널 데이터 구조를 위한 인터페이스가 적절히 정의되었습니다. 내부 타입과 외부 응답 타입을 분리한 구조가 깔끔합니다.
src/features/event-manage/event-create/model/hostCreation.ts (1)
1-6: 호스트 생성 요청 인터페이스가 적절히 정의되었습니다.필요한 모든 필드가 포함되어 있고, 타입 정의가 명확합니다. 외부 모듈에서 사용하기 적합한 형태로 잘 설계되었습니다.
src/widgets/event/api/hostChannelList.ts (1)
14-14: export 방식이 좋습니다.단일 함수에 대한 default export 사용이 적절합니다.
design-system/ui/buttons/IconButton.tsx (3)
7-7: 추가된 iconClassName 속성이 컴포넌트 확장성을 높입니다.아이콘에 추가적인 스타일링을 적용할 수 있도록 선택적 속성을 추가한 것은 좋은 접근법입니다. 이를 통해 컴포넌트 재사용성이 향상됩니다.
10-10: 함수 시그니처가 인터페이스와 일치하도록 적절히 업데이트되었습니다.인터페이스에 추가된
iconClassName속성이 함수 파라미터에도 정확히 반영되어 있습니다.
19-19: 아이콘 스타일 적용이 올바르게 구현되었습니다.
iconClassName이 div 요소의 className에 적절히 통합되어 있어, 추가 스타일이 아이콘 컨테이너에 올바르게 적용됩니다.src/features/dashboard/model/ResponseStore.tsx (1)
43-45: setCurrentIndex 메서드가 간결하게 구현되었습니다.함수형 업데이트를 사용하여 현재 인덱스를 업데이트하는 방식이 적절합니다.
design-system/ui/ChoiceChip.tsx (3)
9-9: buttonClassName 속성 추가로 컴포넌트 유연성이 향상되었습니다.버튼에 추가 스타일링을 적용할 수 있는 속성을 추가한 것은 좋은 접근법입니다. 이로써 컴포넌트의 재사용성이 높아집니다.
12-12: 함수 시그니처가 적절히 업데이트되었습니다.기본값이 빈 문자열로 설정되어 있어 이전 코드와의 호환성을 유지할 수 있습니다.
30-30: 버튼 스타일 적용이 올바르게 구현되었습니다.
buttonClassName이 버튼 요소의 className에 적절히 통합되어 있어 추가 스타일이 올바르게 적용됩니다.src/features/event-manage/event-create/hooks/useHostHook.ts (1)
1-5: 필요한 의존성이 적절히 임포트되었습니다.React Query의
useMutation과 관련 API 및 타입 임포트가 명확하게 정의되어 있습니다.src/widgets/dashboard/ui/ResponsesFilterBar.tsx (2)
1-6: 잘 구성된 ResponsesFilterBarProps 인터페이스입니다.ResponsesFilterBar 컴포넌트의 명확한 타입 정의가 잘 되어 있습니다. 'summary', 'query', 'individual'의 리터럴 타입 사용이 좋습니다.
33-34: export 구문이 적절하게 작성되었습니다.export default 구문이 올바르게 작성되어 있어 컴포넌트를 쉽게 가져와 사용할 수 있습니다.
src/features/event-manage/event-create/ui/TimePicker.tsx (1)
54-55: export 구문이 적절하게 작성되었습니다.export default 구문이 올바르게 작성되어 있어 컴포넌트를 쉽게 가져와 사용할 수 있습니다.
src/features/event-manage/event-create/ui/ShareEventModal.tsx (1)
39-40: export 구문이 적절하게 작성되었습니다.export default 구문이 올바르게 작성되어 있어 컴포넌트를 쉽게 가져와 사용할 수 있습니다.
src/features/dashboard/ui/OptionSection.tsx (1)
49-50: export 구문이 적절하게 작성되었습니다.export default 구문이 올바르게 작성되어 있어 컴포넌트를 쉽게 가져와 사용할 수 있습니다.
src/features/event-manage/event-create/model/FunnelContext.tsx (3)
5-11: FunnelState 인터페이스 구조가 명확합니다
hostState와 eventState, 그리고 세터들을 한 곳에서 관리하기에 직관적이며 확장성도 좋아 보입니다. 별다른 문제 없이 잘 정의되어 있습니다.
42-44: setHostChannelId 함수 사용 시 유의사항
eventState의 hostChannelId만 갱신할 때 의도하지 않은 상태 누락은 없는지 고민해 보면 좋겠습니다. 현재 구조로는 큰 문제 없어 보입니다만, 기능 확장 시 주의해주세요.
53-59: useFunnelState 훅의 예외 처리 확인
Context가 없으면 에러를 던지는 로직은 올바른 패턴입니다. 다른 Provider와 함께 사용될 때에도 문제 없도록 주의하면 더욱 안정적일 것입니다.src/features/event-manage/event-create/ui/EventCategory.tsx (2)
5-14: 인터페이스 정의가 간결하고 명확합니다
Category 및 EventCategoryProps 모두 역할이 분명하게 구분되어 있습니다. 향후 확장 시에도 큰 무리가 없을 것으로 보입니다.
15-29: 초기 상태 관리 흐름이 잘 설정되었습니다
이미 선택된 카테고리가 있는 경우 해당 값을 반영하고, 없을 시 null로 초기화하여 유연하게 처리하고 있습니다. setEventState가 없을 때에도 동작 방해가 없도록 처리한 점도 좋습니다.src/pages/dashboard/ui/ticket/TicketOptionPage.tsx (3)
36-61: defaultData 설정과 localStorage 불러오기 로직
기본 데이터 구조와 로컬 스토리지 값을 합치는 로직이 직관적입니다. 다만, 추후 데이터가 복잡해질 경우, 별도의 유틸 함수를 고려해도 좋겠습니다.
63-90: newOption 추가 시 중복 방지 처리
이미 존재하지 않는 새 옵션만 추가하도록 조건을 둔 점이 좋습니다. 이 로직으로 데이터 무결성을 지키고 있어 보이며, 추가 후 localStorage에 즉시 반영하는 처리도 적절합니다.
94-166: onDragEnd 로직 검토
동일 영역(ticket) 내 재정렬 처리와 options→ticket 영역으로 이동하는 분기 처리가 잘 나뉘어 있습니다. ticket 영역에 이미 존재하는지 확인 후 복사본을 생성하는 방식도 직관적이고 안전합니다.src/shared/ui/DropDown.tsx (4)
1-6: 적절한 인터페이스 정의Option 인터페이스가 명확하게 정의되어 있습니다. 다만, 인터페이스 정의를 공통 타입 파일로 추출하는 것을 고려해보세요.
8-12: 명확한 프롭스 타입 정의프롭스 인터페이스가 잘 정의되어 있습니다. onSelect 콜백의 타입 시그니처가 명확합니다.
14-22: 상태 관리 및 이벤트 핸들러 구현이 적절함useState 훅을 사용한 상태 관리와 이벤트 핸들러 구현이 깔끔합니다.
52-52: export default 적절히 사용컴포넌트가 적절히 기본 내보내기로 설정되어 있습니다.
src/features/event-manage/event-create/ui/EventType.tsx (5)
1-6: 적절한 임포트 구문필요한 컴포넌트와 훅이 잘 임포트되어 있습니다.
8-16: 상태 관리가 잘 구현됨useFunnelState 훅을 사용한 상태 관리와 로컬 상태 관리가 적절히 구현되어 있습니다.
17-45: 이벤트 핸들러 구현이 적절함이벤트 타입 선택, 주소 변경, 상세 주소 변경, 위치 변경에 대한 핸들러가 잘 구현되어 있습니다.
47-72: 이벤트 타입 선택 UI가 직관적으로 구현됨온라인/오프라인 이벤트 타입 선택 UI가 사용자 경험을 고려하여 잘 구현되어 있습니다.
88-89: export default 적절히 사용컴포넌트가 적절히 기본 내보내기로 설정되어 있습니다.
src/features/event-manage/event-create/ui/DatePicker.tsx (5)
1-12: 적절한 임포트 및 인터페이스 정의필요한 모듈들이 잘 임포트되어 있고, DatePickerProps 인터페이스가 명확하게 정의되어 있습니다.
14-21: 초기 상태 설정이 적절함날짜 및 시간 상태의 초기화가 잘 구현되어 있습니다. eventState의 값이 있으면 그것을 사용하고, 없으면 기본값을 설정합니다.
22-40: 유틸리티 함수 구현이 적절함시간 옵션 생성 및 날짜 포맷팅 함수가 잘 구현되어 있습니다.
44-54: useEffect 의존성 배열 관리가 적절함useEffect 훅의 의존성 배열에 필요한 모든 변수가 포함되어 있어 적절합니다.
153-154: export default 적절히 사용컴포넌트가 적절히 기본 내보내기로 설정되어 있습니다.
src/features/payment/ui/TicketOption.tsx (2)
1-7: 필요한 컴포넌트와 훅 임포트필요한 컴포넌트와 훅을 잘 임포트하고 있습니다.
8-11: 상태 관리가 잘 구현됨useTicketOptionStore 훅을 사용한 상태 관리가 적절히 구현되어 있습니다.
src/features/dashboard/ui/SelectedResponseList.tsx (4)
1-5: 적절한 컴포넌트와 타입 import필요한 컴포넌트와 타입들이 적절히 import 되었습니다.
6-17: 인터페이스 정의가 명확함Props와 Response 객체의 인터페이스 정의가 명확하게 되어 있어 컴포넌트의 타입 안정성을 보장합니다.
29-47: selectedResponse 조건 렌더링 로직selectedResponse가 있을 때의 렌더링 로직이 잘 구현되어 있습니다. 주어진 currentIndex에 따라 적절히 응답을 슬라이스하여 보여주고 있습니다.
66-70: 적절한 export 구문컴포넌트 export가 명확하게 되어 있습니다.
src/features/dashboard/ui/ResponseFilter.tsx (5)
1-17: 적절한 imports와 인터페이스 정의필요한 컴포넌트와 자산 파일들이 잘 import 되어 있고, 인터페이스 정의가 상세하게 되어 있어 타입 안정성을 보장합니다.
19-33: 필드 매핑 및 옵션 변환 로직한국어 필드 매핑 로직이 잘 구현되어 있습니다. 옵션을 한국어로 변환하는 과정도 명확합니다.
34-43: 페이지 변경 핸들러 로직페이지 변경을 위한 핸들러 함수가 잘 구현되어 있습니다. 인덱스 범위를 적절히 제한하고, 다음 옵션을 선택하는 로직도 명확합니다.
63-84: 네비게이션 버튼 구현네비게이션 버튼과 그 이벤트 핸들러가 잘 구현되어 있습니다. 리스트 타입에 따라 다른 로직을 적용하는 점도 좋습니다.
85-92: 명확한 컴포넌트 종료 및 export컴포넌트 종료 및 export가 명확하게 되어 있습니다.
src/pages/dashboard/ui/ticket/TicketOptionCreatePage.tsx (12)
1-20: 필요한 imports 및 인터페이스 정의필요한 모듈들을 잘 import하고 있으며, OptionConfig 인터페이스도 명확하게 정의되어 있습니다.
21-61: 상태 관리 및 초기화 로직상태 변수들이 체계적으로 정의되어 있고, 선택된 타입에 따라 다른 옵션 세트를 사용하는 로직이 잘 구현되어 있습니다.
62-109: 활성 옵션 및 설정을 위한 getter/setter 함수getter와 setter 함수를 통해 선택된 타입에 따라 다른 상태를 처리하는 방식이 잘 구현되어 있습니다. 이 접근 방식은 코드 중복을 피하고 유지보수성을 높입니다.
111-130: 이벤트 핸들러 함수 구현이벤트 핸들러 함수들이 잘 구현되어 있습니다. 특히 handleLimitToggled 함수는 이전 코드 리뷰에서 제안된 방식대로 옵션별 제한 설정을 지원합니다.
131-150: 선택지 삭제 기능선택지 삭제 기능이 잘 구현되어 있으며, 최소 한 개 이상의 선택지가 남아있도록 유효성 검사를 수행합니다.
🧰 Tools
🪛 Biome (1.9.4)
[error] 139-139: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.(lint/complexity/noUselessLoneBlockStatements)
151-193: 선택지 추가 및 업데이트 기능선택지 추가 및 입력 변경 기능이 잘 구현되어 있습니다. 유효성 검사를 포함하여 사용자 경험을 개선합니다.
🧰 Tools
🪛 Biome (1.9.4)
[error] 166-168: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.(lint/complexity/noUselessLoneBlockStatements)
[error] 185-186: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.(lint/complexity/noUselessLoneBlockStatements)
194-206: 수량 변경 핸들러수량 변경 핸들러가 잘 구현되어 있습니다. 토글이 켜져 있을 때만 수정을 허용하는 로직이 적절합니다.
208-256: 저장 기능 및 유효성 검사저장 기능이 잘 구현되어 있으며, 필요한 유효성 검사를 수행합니다. 유효하지 않은 경우 적절한 피드백을 제공합니다.
258-291: 유효성 검사를 위한 useEffect 훅useEffect 훅을 사용한 유효성 검사 로직이 잘 구현되어 있습니다. 상태 변경에 따라 경고 메시지를 업데이트합니다.
🧰 Tools
🪛 Biome (1.9.4)
[error] 280-282: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.(lint/complexity/noUselessLoneBlockStatements)
340-402: 레이아웃 및 기본 입력 필드 렌더링DashboardLayout을 사용하여 UI를 구성하고, 질문 및 상세 설명을 위한 입력 필드를 렌더링하는 부분이 잘 구현되어 있습니다.
477-489: 저장 버튼 및 레이아웃 마무리저장 버튼 및 레이아웃 마무리 부분이 잘 구현되어 있습니다.
490-493: 적절한 export컴포넌트 export가 명확하게 되어 있습니다.
🧰 Tools
🪛 Biome (1.9.4)
[error] 463-493: Avoid using unnecessary Fragment.
A fragment is redundant if it contains only one child, or if it is the child of a html element, and is not a keyed fragment.
Unsafe fix: Remove the Fragment(lint/complexity/noUselessFragments)
src/features/event-manage/event-create/ui/LinkInput.tsx (5)
1-11: 적절한 imports 및 인터페이스 정의필요한 모듈과 자산 파일들을 잘 import하고 있으며, LinkInputProps 인터페이스가 명확하게 정의되어 있습니다.
12-22: 상태 변수 설정상태 변수들이 적절하게 정의되어 있습니다. eventState에서 링크 정보를 초기화하고, 사용자 인터랙션을 위한 상태 변수들을 관리합니다.
23-38: 링크 추가 함수새 링크를 추가하는 함수가 잘 구현되어 있으며, 로컬 상태와 전역 상태 모두 적절히 업데이트합니다.
86-115: URL 입력 필드 및 삭제 버튼 구현URL 입력 필드와 삭제 버튼이 잘 구현되어 있습니다. 호버 효과를 통해 삭제 버튼을 보여주는 UX가 직관적입니다.
116-127: 추가 버튼 및 컴포넌트 마무리추가 버튼과 컴포넌트 마무리 부분이 잘 구현되어 있습니다.
src/features/dashboard/ui/DragArea.tsx (1)
63-63: isDropDisabled prop 사용에 대한 검증이 필요합니다.
isDropDisabled={isOptionsArea}로 설정하면 options 영역에는 항목을 드롭할 수 없게 됩니다. 이것이 의도적인 설계인지 확인해 주세요. 티켓 옵션 페이지에서는 options 영역에서 ticket 영역으로만 항목을 이동할 수 있고, 역방향은 허용되지 않는 것이 일반적인 패턴입니다.src/pages/dashboard/ui/ResponsesManagementPage.tsx (1)
14-16: 응답 데이터를 하드코딩된 값으로 설정하고 있습니다.현재 코드에서는 responsesInfo라는 하드코딩된 값을 사용하여 응답을 설정하고 있습니다. 실제 환경에서는 API 호출을 통해 데이터를 가져오는 방식으로 수정해야 합니다.
useEffect(() => { - setResponses(responsesInfo); + // 실제 API 호출로 대체해야 함 + const fetchResponses = async () => { + try { + // const response = await api.getResponses(); + // setResponses(response.data); + setResponses(responsesInfo); // 임시 데이터 사용 + } catch (error) { + console.error("응답을 가져오는 중 오류 발생:", error); + // 에러 처리 로직 추가 + } + }; + + fetchResponses(); }, [setResponses]);src/features/event-manage/event-create/hooks/useFunnelHook.tsx (1)
3-3: 불필요한 import 확인 필요ReactNode를 import 하고 있지만, 타입 선언에만 사용되고 있습니다. 프로젝트 설정에 따라 이 import가 필요하지 않을 수 있습니다.
타입 체크 도구(TypeScript)가 타입 전용 import를 지원하는지 확인하세요. 프로젝트 설정에 따라 다음과 같이 수정할 수 있습니다:
- import { ReactElement, ReactNode, useState } from 'react'; + import { ReactElement, useState } from 'react'; + import type { ReactNode } from 'react';이렇게 하면 번들 크기를 줄이는 데 도움이 될 수 있습니다.
src/shared/ui/AddressSearch.tsx (2)
31-36: LGTM: 상세 주소 변경 핸들러 잘 구현됨handleDetailAddressChange 함수가 잘 구현되어 있습니다. 상세 주소 변경 시 로컬 상태를 업데이트하고, 콜백이 제공된 경우 해당 콜백을 호출하는 로직이 깔끔합니다.
43-47: LGTM: 배경 클릭 시 모달 닫기 기능 잘 구현됨모달 외부(배경) 클릭 시 모달을 닫는 기능이 적절히 구현되어 있습니다. 이벤트 타겟 체크를 통해 정확히 배경을 클릭했을 때만 모달이 닫히도록 처리한 점이 좋습니다.
| @@ -0,0 +1,13 @@ | |||
| import { Map, MapMarker } from 'react-kakao-maps-sdk'; | |||
There was a problem hiding this comment.
글로벌 Map 객체 섀도잉 문제를 해결하세요.
Map은 JavaScript 내장 객체이므로, 이를 가져올 때 이름 충돌 가능성이 있습니다. 이름을 변경하여 잠재적인 혼란을 방지하세요.
-import { Map, MapMarker } from 'react-kakao-maps-sdk';
+import { Map as KakaoMap, MapMarker } from 'react-kakao-maps-sdk';그리고 컴포넌트에서도 변경된 이름을 사용해야 합니다:
- <Map center={{ lat, lng }} style={{ width: '100%', height: '100%' }}>
+ <KakaoMap center={{ lat, lng }} style={{ width: '100%', height: '100%' }}>
<MapMarker position={{ lat, lng }} />
- </Map>
+ </KakaoMap>📝 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.
| import { Map, MapMarker } from 'react-kakao-maps-sdk'; | |
| import { Map as KakaoMap, MapMarker } from 'react-kakao-maps-sdk'; | |
| function MapContainer({ lat, lng }) { | |
| return ( | |
| <KakaoMap center={{ lat, lng }} style={{ width: '100%', height: '100%' }}> | |
| <MapMarker position={{ lat, lng }} /> | |
| </KakaoMap> | |
| ); | |
| } | |
| export default MapContainer; |
🧰 Tools
🪛 Biome (1.9.4)
[error] 1-1: Do not shadow the global "Map" property.
Consider renaming this variable. It's easy to confuse the origin of variables when they're named after a known global.
(lint/suspicious/noShadowRestrictedNames)
| const useHostChannelList = () => { | ||
| const { data, refetch } = useQuery<HostChannelListResponse, AxiosError>({ | ||
| queryKey: ['hostChannelList'], | ||
| queryFn: hostChannelList, | ||
| }); | ||
|
|
||
| return { data, refetch }; | ||
| }; |
There was a problem hiding this comment.
호스트 채널 목록 훅 구현이 미완성되었습니다.
useHostChannelList 훅이 기본적인 기능을 제공하고 있지만, 다음과 같은 중요한 개선이 필요합니다:
- 로딩 상태와 오류 상태를 반환하지 않아 컴포넌트에서 적절한 UI 처리가 어렵습니다.
- 데이터가 없을 때의 기본값 처리가 없습니다.
- 파일 확장자가 JSX를 포함하지 않는데도
.tsx로 되어 있습니다.
const useHostChannelList = () => {
- const { data, refetch } = useQuery<HostChannelListResponse, AxiosError>({
+ const { data, refetch, isLoading, isError, error } = useQuery<HostChannelListResponse, AxiosError>({
queryKey: ['hostChannelList'],
queryFn: hostChannelList,
});
- return { data, refetch };
+ return {
+ data: data?.result || [],
+ refetch,
+ isLoading,
+ isError,
+ error
+ };
};📝 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.
| const useHostChannelList = () => { | |
| const { data, refetch } = useQuery<HostChannelListResponse, AxiosError>({ | |
| queryKey: ['hostChannelList'], | |
| queryFn: hostChannelList, | |
| }); | |
| return { data, refetch }; | |
| }; | |
| const useHostChannelList = () => { | |
| const { data, refetch, isLoading, isError, error } = useQuery<HostChannelListResponse, AxiosError>({ | |
| queryKey: ['hostChannelList'], | |
| queryFn: hostChannelList, | |
| }); | |
| return { | |
| data: data?.result || [], | |
| refetch, | |
| isLoading, | |
| isError, | |
| error | |
| }; | |
| }; |
| export const createHost = async (data: HostCreationRequest) => { | ||
| const response = await axiosClient.post('/host-channels', data); | ||
| return response.data; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
API 함수에 오류 처리와 타입 정의 필요
현재 createHost 함수는 오류 처리가 없고 반환 타입이 정의되어 있지 않습니다. API 호출 중 발생할 수 있는 예외를 적절히 처리하고, 명확한 반환 타입을 지정해야 합니다.
+import { AxiosError } from 'axios';
import { axiosClient } from '../../../../shared/types/api/http-client';
import { HostCreationRequest } from '../model/hostCreation';
-export const createHost = async (data: HostCreationRequest) => {
+// 호스트 채널 응답 타입 정의 (실제 API 응답에 맞게 수정 필요)
+interface HostCreationResponse {
+ id: number;
+ hostChannelName: string;
+ profileImageUrl: string;
+ // 필요한 다른 필드 추가
+}
+
+/**
+ * 호스트 채널을 생성하는 API 함수
+ * @param data 호스트 채널 생성 요청 데이터
+ * @returns 생성된 호스트 채널 정보
+ * @throws 호스트 채널 생성 실패 시 발생하는 예외
+ */
+export const createHost = async (data: HostCreationRequest): Promise<HostCreationResponse> => {
+ try {
const response = await axiosClient.post('/host-channels', data);
return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ console.error('호스트 채널 생성 실패:', axiosError.message);
+ throw error;
+ }
};| export const deleteHost = async (hostChannelId: number) => { | ||
| const response = await axiosClient.delete(`/host-channels/${hostChannelId}`); | ||
| return response.data; | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
호스트 삭제 함수 개선 필요
deleteHost 함수도 오류 처리와 반환 타입 정의가 필요합니다. 또한 hostChannelId 매개변수의 유효성 검사를 추가하면 좋을 것 같습니다.
+// 호스트 채널 삭제 응답 타입 정의
+interface HostDeletionResponse {
+ success: boolean;
+ // 필요한 다른 필드 추가
+}
+
+/**
+ * 호스트 채널을 삭제하는 API 함수
+ * @param hostChannelId 삭제할 호스트 채널 ID
+ * @returns 삭제 결과
+ * @throws 호스트 채널 삭제 실패 시 발생하는 예외
+ */
-export const deleteHost = async (hostChannelId: number) => {
+export const deleteHost = async (hostChannelId: number): Promise<HostDeletionResponse> => {
+ if (!hostChannelId || hostChannelId <= 0) {
+ throw new Error('유효하지 않은 호스트 채널 ID입니다.');
+ }
+
+ try {
const response = await axiosClient.delete(`/host-channels/${hostChannelId}`);
return response.data;
+ } catch (error) {
+ const axiosError = error as AxiosError;
+ console.error('호스트 채널 삭제 실패:', axiosError.message);
+ throw error;
+ }
};📝 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.
| export const deleteHost = async (hostChannelId: number) => { | |
| const response = await axiosClient.delete(`/host-channels/${hostChannelId}`); | |
| return response.data; | |
| }; | |
| // 호스트 채널 삭제 응답 타입 정의 | |
| interface HostDeletionResponse { | |
| success: boolean; | |
| // 필요한 다른 필드 추가 | |
| } | |
| /** | |
| * 호스트 채널을 삭제하는 API 함수 | |
| * @param hostChannelId 삭제할 호스트 채널 ID | |
| * @returns 삭제 결과 | |
| * @throws 호스트 채널 삭제 실패 시 발생하는 예외 | |
| */ | |
| export const deleteHost = async (hostChannelId: number): Promise<HostDeletionResponse> => { | |
| if (!hostChannelId || hostChannelId <= 0) { | |
| throw new Error('유효하지 않은 호스트 채널 ID입니다.'); | |
| } | |
| try { | |
| const response = await axiosClient.delete(`/host-channels/${hostChannelId}`); | |
| return response.data; | |
| } catch (error) { | |
| const axiosError = error as AxiosError; | |
| console.error('호스트 채널 삭제 실패:', axiosError.message); | |
| throw error; | |
| } | |
| }; |
| <div className="flex justify-between text-xs bg-gray-100 shadow-sm px-2 md:px-3 py-3 gap-2" key={response.id}> | ||
| <p>{typeof response[key] === 'object' ? JSON.stringify(response[key]) : response[key]}</p> | ||
| </div> |
There was a problem hiding this comment.
🛠️ Refactor suggestion
타입 안전성 개선 필요
현재 코드에서는 response[key]의 타입을 확인하여 객체인 경우 JSON.stringify를 사용하고 있습니다. 이 방식은 다양한 타입(배열, 함수 등)에 대해 예상치 못한 결과를 가져올 수 있습니다.
객체 타입 체크를 좀 더 명확하게 하고, 다양한 타입을 처리할 수 있도록 개선하세요:
- <p>{typeof response[key] === 'object' ? JSON.stringify(response[key]) : response[key]}</p>
+ <p>{
+ typeof response[key] === 'object' && response[key] !== null
+ ? (Array.isArray(response[key])
+ ? response[key].join(', ')
+ : JSON.stringify(response[key]))
+ : String(response[key])
+ }</p>이렇게 하면 배열은 쉼표로 구분된 문자열로, 다른 객체는 JSON 문자열로, null을 포함한 다른 값들은 문자열로 변환됩니다.
📝 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.
| <div className="flex justify-between text-xs bg-gray-100 shadow-sm px-2 md:px-3 py-3 gap-2" key={response.id}> | |
| <p>{typeof response[key] === 'object' ? JSON.stringify(response[key]) : response[key]}</p> | |
| </div> | |
| <div className="flex justify-between text-xs bg-gray-100 shadow-sm px-2 md:px-3 py-3 gap-2" key={response.id}> | |
| <p>{ | |
| typeof response[key] === 'object' && response[key] !== null | |
| ? (Array.isArray(response[key]) | |
| ? response[key].join(', ') | |
| : JSON.stringify(response[key])) | |
| : String(response[key]) | |
| }</p> | |
| </div> |
| const handleHostCreation = () => { | ||
| createHost(hostState, { | ||
| onSuccess: () => { | ||
| handleNext(String(currentStep - 1)); | ||
| }, | ||
| }); | ||
| }; |
There was a problem hiding this comment.
호스트 생성 실패 처리 누락
handleHostCreation 함수에서는 성공 케이스만 처리하고 있으며, 실패 시 처리가 누락되어 있습니다. 이는 API 호출 실패 시 사용자에게 아무런 피드백이 없게 만듭니다.
onError 핸들러를 추가하여 실패 시 적절한 피드백을 제공하도록 개선하세요:
const handleHostCreation = () => {
createHost(hostState, {
onSuccess: () => {
handleNext(String(currentStep - 1));
},
+ onError: (error) => {
+ console.error('호스트 생성 실패:', error);
+ // 사용자에게 에러 메시지 표시
+ // showErrorToast('호스트 생성에 실패했습니다. 다시 시도해주세요.');
+ }
});
};📝 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.
| const handleHostCreation = () => { | |
| createHost(hostState, { | |
| onSuccess: () => { | |
| handleNext(String(currentStep - 1)); | |
| }, | |
| }); | |
| }; | |
| const handleHostCreation = () => { | |
| createHost(hostState, { | |
| onSuccess: () => { | |
| handleNext(String(currentStep - 1)); | |
| }, | |
| onError: (error) => { | |
| console.error('호스트 생성 실패:', error); | |
| // 사용자에게 에러 메시지 표시 | |
| // showErrorToast('호스트 생성에 실패했습니다. 다시 시도해주세요.'); | |
| } | |
| }); | |
| }; |
f9d2c7d to
ac86f88
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/features/dashboard/ui/DragArea.tsx (1)
28-30: 🛠️ Refactor suggestion미사용 props 제거 필요
이전 리뷰와 동일하게
answerToggled와responseFormat이 실제로 사용되지 않아 ESLint에서도 지적되고 있습니다. 아래와 같이 인터페이스와 구조분해 할당에서 제거해 주세요.interface DragAreaProps { data: { /* ... */ }; setData: React.Dispatch<React.SetStateAction<DragAreaProps['data']>>; droppableId: string; - answerToggled: boolean; - responseFormat: string; ticketSurveyAddButton?: boolean; } const DragArea = ({ data, setData, droppableId, - answerToggled, - responseFormat, ticketSurveyAddButton = true, }: DragAreaProps) => {Also applies to: 37-38
🧹 Nitpick comments (6)
src/app/routes/Router.tsx (1)
36-36: 사용되지 않는TicketOptionAttachPageimport입니다.
현재 import만 되어 있고 실제로 사용하지 않으므로 제거하거나 앞으로 사용할 계획이라면 관련 로직을 추가하세요.아래 예시처럼 미사용 import를 제거할 수 있습니다:
-import TicketOptionAttachPage from '../../pages/dashboard/ui/ticket/TicketOptionAttachPage';🧰 Tools
🪛 ESLint
[error] 36-36: 'TicketOptionAttachPage' is defined but never used.
(@typescript-eslint/no-unused-vars)
src/pages/dashboard/ui/ticket/TicketOptionPage.tsx (3)
37-37:localStorage데이터 파싱 시 예외 처리가 필요합니다.
JSON.parse과정에서 발생 가능한 예외가 발생하면 전체 로직이 중단될 위험이 있으므로, try-catch 구문 등을 활용하여 안전성을 높이는 것이 좋습니다.- const savedData = localStorage.getItem('ticketOptions'); - let initialData: Data = savedData ? JSON.parse(savedData) : defaultData; + const savedData = localStorage.getItem('ticketOptions'); + let initialData: Data; + try { + initialData = savedData ? JSON.parse(savedData) : defaultData; + } catch (error) { + console.error('ticketOptions JSON 파싱 실패:', error); + initialData = defaultData; + }
179-179:delete연산자 사용 대신 속성을undefined로 설정하는 방식을 고려해주세요.
delete는 성능에 부정적 영향을 줄 수 있고, 객체 최적화를 방해할 가능성이 있습니다.- delete newState.newOption; + newState.newOption = undefined;Also applies to: 223-223
255-255:childrenProp 대신 JSX 요소를 활용해보세요.
React에서는children을 바로 JSX로 전달하는 패턴이 일반적입니다.-<IconText iconPath={<img src={Ticket} alt="추가 버튼" />} children="티켓" className="font-bold pl-2" /> +<IconText iconPath={<img src={Ticket} alt="추가 버튼" />} className="font-bold pl-2"> + 티켓 +</IconText>src/pages/dashboard/ui/ticket/TicketOptionCreatePage.tsx (2)
131-133: 불필요한 블록 제거 권장아래 구간들은 단일 주석만 존재하는 블록으로, 코드 흐름에 영향을 주지 않으므로 제거를 권장합니다. (예: 131-133, 152-154, 175-177, 258-260, 275-277)
-{ - /*선택지 삭제*/ -} const handleClearOption = (index: number) => { // ... } -{ - /*선택지 추가*/ -} const handleAddOption = () => { // ... } -{ - /*선택지 입력 업데이트*/ -} const handleInputChange = (index: number, value: string) => { // ... } -{ - /*선택지 입력 시 경고 메시지 제거*/ -} -{ - /*수량 입력 시 경고 메시지 제거*/ -}Also applies to: 152-154, 175-177, 258-260, 275-277
200-205: 수량을 숫자로 안전하게 관리하기
quantity가 문자열로 그대로 저장되고 있습니다. 실제로 숫자만을 다룬다면parseInt등의 방식을 통해 안전하게 변환 후 상태에 반영하는 방안을 고려해 주세요.if (updated[index].limitToggled) { - updated[index].quantity = value; // 문자열 그대로 저장 + updated[index].quantity = value.replace(/\D+/g, ''); // 숫자 이외 문자 제거 후 저장 예시 }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (8)
public/assets/dashboard/ticket/AddButton2.svgis excluded by!**/*.svgpublic/assets/dashboard/ticket/ModifyPencilIcon.svgis excluded by!**/*.svgpublic/assets/dashboard/ticket/Option.svgis excluded by!**/*.svgpublic/assets/dashboard/ticket/deleteIcon.svgis excluded by!**/*.svgpublic/assets/dashboard/ticket/option(black).svgis excluded by!**/*.svgpublic/assets/dashboard/ticket/option(pink).svgis excluded by!**/*.svgyarn 2.lockis excluded by!**/*.lockyarn.lockis excluded by!**/yarn.lock,!**/*.lock
📒 Files selected for processing (15)
design-system/ui/ChoiceChip.tsx(2 hunks)design-system/ui/buttons/IconButton.tsx(2 hunks)design-system/ui/buttons/TertiaryButton.tsx(1 hunks)design-system/ui/textFields/DefaultTextField.tsx(3 hunks)design-system/ui/texts/IconText.tsx(1 hunks)package.json(2 hunks)src/app/routes/Router.tsx(3 hunks)src/app/routes/routes.ts(1 hunks)src/features/dashboard/ui/Checklist.tsx(1 hunks)src/features/dashboard/ui/DragArea.tsx(1 hunks)src/features/dashboard/ui/DraggableList.tsx(1 hunks)src/pages/dashboard/ui/ticket/TicketOptionCreatePage.tsx(1 hunks)src/pages/dashboard/ui/ticket/TicketOptionPage.tsx(1 hunks)src/shared/types/dashboardType.ts(2 hunks)tsconfig.json(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (9)
- design-system/ui/texts/IconText.tsx
- src/shared/types/dashboardType.ts
- src/app/routes/routes.ts
- design-system/ui/buttons/TertiaryButton.tsx
- tsconfig.json
- package.json
- design-system/ui/buttons/IconButton.tsx
- design-system/ui/textFields/DefaultTextField.tsx
- design-system/ui/ChoiceChip.tsx
🧰 Additional context used
🧬 Code Definitions (1)
src/app/routes/Router.tsx (1)
src/app/routes/routes.ts (1)
DASHBOARD_ROUTES(32-46)
🪛 Biome (1.9.4)
src/pages/dashboard/ui/ticket/TicketOptionCreatePage.tsx
[error] 139-139: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
[error] 166-168: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
[error] 185-186: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
[error] 280-282: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
[error] 296-299: This block statement doesn't serve any purpose and can be safely removed.
Standalone block statements without any block-level declarations are redundant in JavaScript and can be removed to simplify the code.
Safe fix: Remove redundant block.
(lint/complexity/noUselessLoneBlockStatements)
src/pages/dashboard/ui/ticket/TicketOptionPage.tsx
[error] 194-195: Change to an optional chain.
Unsafe fix: Change to an optional chain.
(lint/complexity/useOptionalChain)
[error] 197-197: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
[error] 239-240: Avoid the delete operator which can impact performance.
Unsafe fix: Use an undefined assignment instead.
(lint/performance/noDelete)
[error] 258-259: Avoid passing children using a prop
The canonical way to pass children in React is to use JSX elements
(lint/correctness/noChildrenProp)
[error] 275-275: Avoid passing children using a prop
The canonical way to pass children in React is to use JSX elements
(lint/correctness/noChildrenProp)
🪛 ESLint
src/features/dashboard/ui/DragArea.tsx
[error] 37-37: 'answerToggled' is defined but never used.
(@typescript-eslint/no-unused-vars)
[error] 38-38: 'responseFormat' is defined but never used.
(@typescript-eslint/no-unused-vars)
src/app/routes/Router.tsx
[error] 36-36: 'TicketOptionAttachPage' is defined but never used.
(@typescript-eslint/no-unused-vars)
src/features/dashboard/ui/Checklist.tsx
[error] 7-7: 'checkLists' is assigned a value but never used.
(@typescript-eslint/no-unused-vars)
🔇 Additional comments (7)
src/app/routes/Router.tsx (3)
32-33: 추가된 컴포넌트 Import가 정상적으로 이루어졌습니다.
티켓 옵션 관련 페이지를 라우트에서 사용하기 위해TicketOptionPage,TicketOptionCreatePage를 불러오는 처리는 올바릅니다.
69-70: 새로운 라우트 설정이 적절합니다.
ticketOption,ticketOptionCreate경로 모두 인증 없이 접근 가능하다고 설정되어 있으며, 대응되는 컴포넌트와 일관성이 있습니다.
93-93: 변경 사항이 없어 별다른 조치가 필요 없습니다.
라우터 기본 내보내기 부분은 기존 구조와 충돌하지 않습니다.src/features/dashboard/ui/DraggableList.tsx (3)
7-16: 타입 정의가 명확하게 잘 구성되었습니다.
DraggableListProps인터페이스에서 필요한 필드를 명시하여, 컴포넌트 사용 시 혼동을 줄일 수 있습니다.
30-50:localStorage파싱 오류 시 예외 처리가 필요합니다.
이전에 동일한 지적 사항이 있었으나 아직 try-catch 처리가 없습니다.JSON.parse단계에서 발생할 수 있는 예외를 방어적으로 처리하면 좋겠습니다.아래 예시처럼 방어 로직을 추가해보세요:
const handleEditClick = () => { // localStorage에서 전체 데이터 가져오기 const savedData = localStorage.getItem('ticketOptions'); - const parsedData = savedData ? JSON.parse(savedData) : null; + let parsedData = null; + try { + parsedData = savedData ? JSON.parse(savedData) : null; + } catch (error) { + console.error('ticketOptions 파싱 실패:', error); + } const optionData = parsedData?.options?.[id]; ... };
53-85: 드래그 앤 드롭 기능이 올바르게 구현되었습니다.
Draggable컴포넌트를 적절히 사용하여 시각적 인터랙션과 동시에 상태를 관리하고 있습니다. 현재 로직에 문제는 없어 보입니다.src/pages/dashboard/ui/ticket/TicketOptionCreatePage.tsx (1)
475-475: '주관식'(자유로운 텍스트) 처리 미완성주석에 표시된 것처럼 '자유로운 텍스트' 응답 형식에 대한 구현이 비어 있습니다. 과거 리뷰에서도 지적된 부분이므로, 필요한 UI와 로직을 완성해 주시면 좋겠습니다.
다음 예시처럼 옵션 입력을 위한 컴포넌트를 추가하고, 이후 입력 결과를 저장하는 로직까지 구현을 고려하실 수 있습니다:
{selectedChip === '자유로운 텍스트' && ( <div> - {/*수정해야할 부분*/} + <DefaultTextField + placeholder="자유롭게 입력해주세요." + className="h-12 mb-5" + /> </div> )}
| const checkLists = menuLists | ||
| .filter(item => ['이벤트 기본 정보', '이벤트 상세와 사진', '티켓 생성하기', '티켓에 추가 옵션 부착'].includes(item.text)) | ||
| .map(items => ({ text: items.text, path: items.path })); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
중복된 checkLists 선언으로 인해 사용되지 않는 변수입니다.
첫 번째 checkLists(7-9행)는 17행 이후에 다시 선언되는 동일 이름의 checkLists 변수로 덮어씌워지고 있어 ESLint에서 미사용 변수가 된 것으로 보입니다. 두 선언 중 하나만 사용하거나, 로직을 통합해 불필요한 중복을 제거하세요.
아래와 같은 방식으로 기존 checkLists 필드를 통합해보세요:
-// 7~9행 선언부 제거
-const checkLists = menuLists
- .filter(item => ['이벤트 기본 정보', '이벤트 상세와 사진', '티켓 생성하기', '티켓에 추가 옵션 부착'].includes(item.text))
- .map(items => ({ text: items.text, path: items.path }));
// 17행 이후 선언을 확장하여 새로운 항목까지 포함
const checkLists = menuLists
.filter(item =>
['이벤트 기본 정보', '이벤트 상세와 사진', '티켓 생성하기', '티켓에 추가 옵션 부착'].includes(item.text),
)
.map(items => ({ text: items.text, path: items.path }));📝 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.
| const checkLists = menuLists | |
| .filter(item => ['이벤트 기본 정보', '이벤트 상세와 사진', '티켓 생성하기', '티켓에 추가 옵션 부착'].includes(item.text)) | |
| .map(items => ({ text: items.text, path: items.path })); | |
| // Removed duplicate checkLists declaration from earlier in the file | |
| // 17행 이후 선언을 확장하여 새로운 항목까지 포함 | |
| const checkLists = menuLists | |
| .filter(item => | |
| ['이벤트 기본 정보', '이벤트 상세와 사진', '티켓 생성하기', '티켓에 추가 옵션 부착'].includes(item.text), | |
| ) | |
| .map(item => ({ text: item.text, path: item.path })); |
🧰 Tools
🪛 ESLint
[error] 7-7: 'checkLists' is assigned a value but never used.
(@typescript-eslint/no-unused-vars)
1. 호스트 대시보드 티켓에 추가 옵션 부착 추가
2. 티켓에 추가 옵션 부착 하기 드래그앤 드롭 구현 중
3. 티켓 옵션 생성페이지 퍼블리싱
이후 저장하기 누르면 다시 티켓에 추가 옵션 부착하기 페이지로 이동(드래그앤드롭)
Summary by CodeRabbit