-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 사용자 응답 관리 페이지 퍼블리싱 #64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit
Hold shift + click to select a range
3808258
feat: 사용자 응답관리 요약 퍼블리싱
hyeeuncho 7382e02
feat: 사용자 응답관리 드롭다운 필터링
hyeeuncho 6533e69
feat: 페이징 추가
hyeeuncho 9967a80
feat: 개별 조회 퍼블리싱
hyeeuncho 4434238
design: 개별 조회 CSS 수정
hyeeuncho 291f72d
fix: UnderlineTextField.stories.tsx githubaction 에러 수정
hyeeuncho b451dcf
rename: DropDown 파일 위치 이동
hyeeuncho b156480
design: DropDown CSS 수정
hyeeuncho e69f955
design: DropDown CSS 수정
hyeeuncho bc95285
refact: responses 상태 초기화 변경
hyeeuncho 716f39b
refact: option 기능 분리
hyeeuncho 9f62189
refact: 페이지네이션 currentIndex 방식으로 변경
hyeeuncho 9e74a4e
refact: zustand로 상태관리
hyeeuncho 3cea153
refact: useResponseStore 항목 추가
hyeeuncho dc44217
refact: ResponseList 파일 분리
hyeeuncho 7ce7a1c
refact: SelecetedResponseList 코드 가독성 개선
hyeeuncho 52d70fb
remove: 파일 삭제
hyeeuncho 31a3233
refact: participant페이지와 Response페이지 연결 및 수정
hyeeuncho dfafac7
design: 참여정보 글씨 추가
hyeeuncho 64d71a2
refact: 개별 조회 로직 변경
hyeeuncho a38fc77
refact: 개별 조회 전체 옵션 추가
hyeeuncho 75642aa
refact: page길이 수정
hyeeuncho 7a8df75
refact: selectedField 초기값 설정 변경
hyeeuncho c6b7210
refact: queryOption 동적으로 처리
hyeeuncho b197982
refact: fieldmap 동적 생성
hyeeuncho e05cb0d
design: button 디자인 변경
hyeeuncho 11486cb
refact: 페이지 로직 수정
hyeeuncho f793ffa
design: 반응형 적용
hyeeuncho File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,4 +37,4 @@ export const Default: Story = { | |
| onChange: e => console.log(e.target.value), | ||
| placeholder: '입력해주세요', | ||
| }, | ||
| }; | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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), | ||
| })), | ||
|
|
||
| })); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.