Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3808258
feat: 사용자 응답관리 요약 퍼블리싱
hyeeuncho Mar 7, 2025
7382e02
feat: 사용자 응답관리 드롭다운 필터링
hyeeuncho Mar 7, 2025
6533e69
feat: 페이징 추가
hyeeuncho Mar 9, 2025
9967a80
feat: 개별 조회 퍼블리싱
hyeeuncho Mar 9, 2025
4434238
design: 개별 조회 CSS 수정
hyeeuncho Mar 10, 2025
291f72d
fix: UnderlineTextField.stories.tsx githubaction 에러 수정
hyeeuncho Mar 10, 2025
b451dcf
rename: DropDown 파일 위치 이동
hyeeuncho Mar 11, 2025
b156480
design: DropDown CSS 수정
hyeeuncho Mar 11, 2025
e69f955
design: DropDown CSS 수정
hyeeuncho Mar 11, 2025
bc95285
refact: responses 상태 초기화 변경
hyeeuncho Mar 11, 2025
716f39b
refact: option 기능 분리
hyeeuncho Mar 11, 2025
9f62189
refact: 페이지네이션 currentIndex 방식으로 변경
hyeeuncho Mar 11, 2025
9e74a4e
refact: zustand로 상태관리
hyeeuncho Mar 12, 2025
3cea153
refact: useResponseStore 항목 추가
hyeeuncho Mar 12, 2025
dc44217
refact: ResponseList 파일 분리
hyeeuncho Mar 12, 2025
7ce7a1c
refact: SelecetedResponseList 코드 가독성 개선
hyeeuncho Mar 12, 2025
52d70fb
remove: 파일 삭제
hyeeuncho Mar 12, 2025
31a3233
refact: participant페이지와 Response페이지 연결 및 수정
hyeeuncho Mar 12, 2025
dfafac7
design: 참여정보 글씨 추가
hyeeuncho Mar 13, 2025
64d71a2
refact: 개별 조회 로직 변경
hyeeuncho Mar 13, 2025
a38fc77
refact: 개별 조회 전체 옵션 추가
hyeeuncho Mar 13, 2025
75642aa
refact: page길이 수정
hyeeuncho Mar 13, 2025
7a8df75
refact: selectedField 초기값 설정 변경
hyeeuncho Mar 15, 2025
c6b7210
refact: queryOption 동적으로 처리
hyeeuncho Mar 15, 2025
b197982
refact: fieldmap 동적 생성
hyeeuncho Mar 15, 2025
e05cb0d
design: button 디자인 변경
hyeeuncho Mar 15, 2025
11486cb
refact: 페이지 로직 수정
hyeeuncho Mar 15, 2025
f793ffa
design: 반응형 적용
hyeeuncho Mar 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,4 @@ export const Default: Story = {
onChange: e => console.log(e.target.value),
placeholder: '입력해주세요',
},
};
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"tailwind-scrollbar-hide": "^2.0.0",
"tailwindcss": "^3.4.15",
"zod": "^3.24.2",
"zustand": "^5.0.2"
"zustand": "^5.0.3"
},
"devDependencies": {
"@chromatic-com/storybook": "^3.2.2",
Expand Down
2 changes: 2 additions & 0 deletions src/app/routes/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import MailBoxPage from '../../pages/dashboard/ui/mail/MailBoxPage';
import EmailEditPage from '../../pages/dashboard/ui/mail/EmailEditPage';
import PaymentPage from '../../pages/payment/ui/PaymentPage';
import TicketConfirmPage from '../../pages/dashboard/ui/ticket/TIcketConfirmPage';
import ResponseManagementPage from '../../pages/dashboard/ui/ResponsesManagementPage';

const mainRoutes = [
{ path: MAIN_ROUTES.main, element: <MainPage />, requiresAuth: false },
Expand Down Expand Up @@ -65,6 +66,7 @@ const dashboardRoutes = [
{ path: DASHBOARD_ROUTES.mailBox, element: <MailBoxPage />, requiresAuth: false },
{ path: DASHBOARD_ROUTES.emailEdit, element: <EmailEditPage />, requiresAuth: false },
{ path: DASHBOARD_ROUTES.participantsMangement, element: <ParticipantsManagementPage />, requiresAuth: false },
{ path: DASHBOARD_ROUTES.responsesManagement, element: <ResponseManagementPage />, requiresAuth: false },
];

const paymentRoutes = [
Expand Down
1 change: 1 addition & 0 deletions src/app/routes/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const DASHBOARD_ROUTES = {
mailBox: `${MAIN_ROUTES.dashboard}/mailBox`,
emailEdit: `${MAIN_ROUTES.dashboard}/edit-email`,
participantsMangement: `${MAIN_ROUTES.dashboard}/participants-mangement`,
responsesManagement:`${MAIN_ROUTES.dashboard}/responses-management`,
};

export const PAYMENT_ROUTES = {
Expand Down
47 changes: 47 additions & 0 deletions src/features/dashboard/model/ResponseStore.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { create } from 'zustand';
import { responsesData } from '../../../shared/types/responseType';

interface ResponseState {
response: responsesData[];
selectedField: string;
selectedResponse: responsesData[];
currentIndex: number;

setResponses: (responses: responsesData[]) => void;
setSelectedField: (field: string) => void;
setSelectedResponse: (responseName: string, responseEmail: string) => void;
setCurrentIndex: (updateFn: (prevIndex: number) => number) => void;
}

export const useResponseStore = create<ResponseState>((set) => ({
response: [],
selectedField: '',
selectedResponse: [],
currentIndex: 0,

setResponses: (response) => {
set(() => ({
response: response,
selectedField: response.length > 0 ? Object.keys(response[0])[1] : ''
}));
},

setSelectedField: (field) => {
set({ selectedField: field });
},

setSelectedResponse: (responseName, responseEmail) => {
set((state) => {
const filteredResponses = state.response.filter((res) => res.name === responseName && res.email === responseEmail);
return {
selectedResponse: filteredResponses,
currentIndex: 0,
};
});
},

setCurrentIndex: (updateFn) => set((state) => ({
currentIndex: updateFn(state.currentIndex),
})),

}));
49 changes: 49 additions & 0 deletions src/features/dashboard/ui/OptionSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import Checkbox from "../../../../design-system/ui/Checkbox";
import { Option } from "../../../shared/types/responseType";

interface Response {
id: string;
name: string;
selectedOptions: {
[key: string]: string;
};
}

interface OptionSectionProps {
responses: Response[];
options: Option[];
}

const OptionSection = ({ responses, options }: OptionSectionProps) => {
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>
);
})}
</div>
))}
</div>
);
};

export default OptionSection;
15 changes: 12 additions & 3 deletions src/features/dashboard/ui/ParicipantCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import TertiaryButton from '../../../../design-system/ui/buttons/TertiaryButton'
import Checkbox from '../../../../design-system/ui/Checkbox';
import { useParticipantStore } from '../model/ParticipantStore';
import { participantsData } from '../../../shared/types/participantInfoType';
import SecondaryButton from '../../../../design-system/ui/buttons/SecondaryButton';
import { useNavigate } from 'react-router-dom';

interface ParticipantCardProps {
participant: participantsData;
Expand All @@ -11,6 +13,7 @@ interface ParticipantCardProps {

const ParticipantCard = ({ participant, checked, onChange }: ParticipantCardProps) => {
const { approvedParticipants, toggleApproveParticipant } = useParticipantStore();
const navigate = useNavigate();
return (
<div className="flex items-center justify-between w-full text-xs bg-white px-2 md:px-3 py-2 shadow-sm">
<div className="flex gap-2 md:gap-3">
Expand All @@ -26,9 +29,15 @@ const ParticipantCard = ({ participant, checked, onChange }: ParticipantCardProp
</div>
</div>
</div>
<div className="flex items-center justify-center gap-4">
{/* ver2에서 넣어야 할 버튼
<SecondaryButton label="확인하기" color="pink" size="small" /> */}
<div className="flex items-center justify-center gap-3">
{<SecondaryButton label="확인하기" color="pink" size="small" onClick={() => {
navigate("/dashboard/responses-management", {
state: {
participantName: participant.name,
participantEmail: participant.email,
},
});
}} />}
{participant.checkIn ? <p className="text-[#888686]">완료</p> : <p className="text-[#888686]">미완료</p>}

{approvedParticipants[participant.ticketNum] ? (
Expand Down
4 changes: 2 additions & 2 deletions src/features/dashboard/ui/PariticipantsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ const ParticipantsList = ({ listType, selectedFilter = [] }: ParticipantsListPro
<div className="flex gap-2 md:gap-3">
<Checkbox checked={all} onChange={toggleAll} label="" />
<div className="flex items-center gap-15 md:gap-24">
<p>티켓 번호</p>
<p>주문 번호</p>
<p>참여자 정보</p>
</div>
</div>
<div className="flex items-center gap-4">
{/* <p>참여자 정보</p> */}
<p>참여자 정보</p>
<p>체크인</p>
<p className="mr-1 md:mr-2">승인</p>
</div>
Expand Down
91 changes: 91 additions & 0 deletions src/features/dashboard/ui/ResponseFilter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import IconButton from '../../../../design-system/ui/buttons/IconButton';
import { responsesData } from '../../../shared/types/responseType';
import DropDown from '../../../shared/ui/DropDown';
import { createFieldMappings } from '../../lib/createFieldMappings';
import rightButton from '../../../../public/assets/main/RightButton.svg';
import leftButton from '../../../../public/assets/main/LeftButton.svg';

interface ResponseFilterProps {
responses: responsesData[];
listType: 'summary' | 'query' | 'individual';
selectedField: { v1: string; v2: string };
setSelectedField: (v1: string, v2: string) => void;
setCurrentIndex: (updateFn: (prevIndex: number) => number) => void;
currentIndex: number;
responsesLength: number;
options: { v1: string; v2: string }[];
}

const ResponseFilter = ({
responses,
listType,
selectedField,
setSelectedField,
setCurrentIndex,
currentIndex,
responsesLength,
options
}: ResponseFilterProps) => {
const { fieldMapToKorean } = createFieldMappings(responses);
const optionsToKorean = options.map(option => ({
v1: fieldMapToKorean[option.v1] || option.v1,
v2: option.v2
}));
const handlePageChange = (direction: 'prev' | 'next') => {
const nextIndex = direction === 'prev' ? Math.max(currentIndex - 1, 0) : Math.min(currentIndex + 1, responsesLength - 1);
setCurrentIndex(() => nextIndex);

const nextOption = options[nextIndex];

if (nextOption) {
setSelectedField(nextOption.v1, nextOption.v2);
}
};
return (
<div className="bg-white p-4 flex flex-col gap-2 mb-4">
<div className="flex justify-between items-center">
<div className="w-2/3">
<DropDown
options={optionsToKorean}
selectedValue={selectedField.v1}
onSelect={(selectedName, selectedEmail) => {
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);
}
}}
/>
</div>
<div className="flex items-center gap-1 md:gap-2 ml-auto ">
<IconButton
iconPath={<img src={leftButton} alt="왼쪽 버튼" />}
onClick={() => {
if (listType === 'individual') {
setCurrentIndex((prev) => Math.max(prev - 1, 0));
} else {
handlePageChange('prev');
}
}}
/>
<span className='text-sm md:text-base w-10 text-center'>{Math.floor(currentIndex / 1) + 1} / {Math.ceil(responsesLength / 1)}</span>
<IconButton
iconPath={<img src={rightButton} alt="오른쪽 버튼" />}
onClick={() => {
if (listType === 'individual') {
setCurrentIndex((prev) => Math.min(prev + 1, responsesLength - 1));
} else {
handlePageChange('next');
}
}}
/>
</div>
</div>
</div>
);
};

export default ResponseFilter;
112 changes: 112 additions & 0 deletions src/features/dashboard/ui/ResponsesList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useResponseStore } from '../model/ResponseStore';
import { responsesInfo } from '../../../shared/types/responseType';
import ResponseFilter from './ResponseFilter';
import SelectedResponseList from './SelectedResponseList';
import { createFieldMappings } from '../../lib/createFieldMappings';
import { useEffect } from 'react';

interface ResponsesListProps {
listType: 'summary' | 'query' | 'individual';
}

const ResponsesList = ({ listType }: ResponsesListProps) => {
const { response, selectedField, setSelectedField, selectedResponse, setSelectedResponse, currentIndex, setCurrentIndex } = useResponseStore();
const { fieldMap, fieldMapToKorean } = createFieldMappings(response);
const queryOptions = response && response[0]
? Object.keys(response[0])
.filter(key => key !== 'id' && key !== 'selectedOptions')
.map(key => ({
v1: fieldMap[key] || key,
v2: ""
}))
: [];

useEffect(() => {
setCurrentIndex(() => 0);
}, [listType, setCurrentIndex]);

const renderSection = (title: string, key: keyof typeof responsesInfo[0], isSummaryPage: boolean) => {
const transTitle = fieldMapToKorean[title];

return (
<div className="bg-white p-4 flex flex-col gap-2 mb-4">
<div className="flex justify-between items-center text-xs bg-white px-2 md:px-3 py-3">
<p className='text-base font-bold'>{transTitle}</p>
<p>응답 {response.length}개</p>
</div>

{response.length === 0 ? (
<p>응답이 없습니다.</p>
) : (
<div className={isSummaryPage ? "h-full max-h-48 overflow-y-auto space-y-2" : "h-full overflow-y-auto space-y-2"}>
{response.map((response) => (
<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>
)}
</div>
);
};

const renderList = () => {
switch (listType) {
case 'summary':
return (
<>
{renderSection('name', 'name', true)}
{renderSection('phone', 'phone', true)}
{renderSection('email', 'email', true)}
{renderSection('grade', 'grade', true)}
{renderSection('email', 'email', true)}
</>
);
case 'query':
return (
<>
<ResponseFilter
responses={response}
listType={listType}
selectedField={{ v1: fieldMapToKorean[selectedField], v2: "" }}
setSelectedField={setSelectedField}
setCurrentIndex={setCurrentIndex}
currentIndex={currentIndex}
responsesLength={queryOptions.length}
options={queryOptions}
/>
{renderSection(selectedField, selectedField as keyof typeof responsesInfo[0], false)}
</>
);
case 'individual':
return (
<div>
<ResponseFilter
responses={response}
listType={listType}
selectedField={selectedResponse.length > 0 ?
{ v1: selectedResponse[0].name, v2: selectedResponse[0].email } : { v1: '전체', v2: '' }}
setSelectedField={setSelectedResponse}
setCurrentIndex={setCurrentIndex}
currentIndex={currentIndex}
responsesLength={selectedResponse.length > 0 ? selectedResponse.length : response.length}
options={[{ v1: '전체', v2: '' },
...response.map((res) => ({ v1: res.name, v2: res.email }))]}
/>
<SelectedResponseList
currentIndex={currentIndex}
/>
</div>
);
default:
return null;
}
};
return (
<div>
{renderList()}
</div>
);
};

export default ResponsesList;
Loading