-
Notifications
You must be signed in to change notification settings - Fork 1
[FEAT] 강사 수정사항 요청 페이지 퍼블리싱 #30
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
19 commits
Select commit
Hold shift + click to select a range
9ece139
#29 [FEAT] 수정 요청 페이지 동적 라우팅 및 사이드바 활성화 유지
waldls 23b1601
#29 [FEAT] 썸네일 컴포넌트 구현
waldls ef82bf5
#29 [FEAT] 수정 요청 초기화면 퍼블리싱
waldls d0f23b9
#29 [FEAT] 최종 시안 선택 모달 연결
waldls 9206491
#29 [FEAT] 선택한 카테고리에 맞는 인풋 섹션 생기도록 구현
waldls be0422c
#29 [FEAT] 코멘트 카드 컴포넌트 구현
waldls a12a814
#29 [REFACTOR] 썸네일 컴포넌트 W/H props로 크기 제어
waldls 6d720f6
#29 [REFACTOR] 썸네일 컴포넌트 className으로 크기 제어하도록 수정
waldls 6074b18
#29 [FEAT] mockData 기반 구조 수정 및 잔여 횟수 0회일 때 렌더링 처리
waldls a026c3c
#29 [FEAT] 디자이너 코멘트 유무에 따른 레이아웃 분기 처리
waldls 725e6d0
#29 [MOVE] Modal 컴포넌트 modal 폴더 아래로 이동
waldls 447f1aa
#29 [FEAT] 시안 확인 모달 구현
waldls bf87839
#29 [REFACTOR] 시안 모달 API 명세에 맞게 변경
waldls ce4395a
#29 [REFACTOR] 시안 모달에 실제 시안 제목/이미지 목록 연결
waldls bb70826
#29 [FEAT] 시안 모달 이미지 우클릭 및 드래그 저장 방지 처리
waldls 29edd1d
#29 [CHORE] mockData 수정
waldls 9ee457e
#29 [FIX] 시안 모달 이미지 sizes 속성 추가
waldls f1e4e79
#29 [REFACTOR] 불필요한 overlay-button 컬러 토큰 제거 후 bg-white/18로 대체
waldls b4a7a05
#29 [REFACTOR] 시안 모달 접근성 보완 (role/aria-label 추가)
waldls 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
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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,108 @@ | ||
| "use client"; | ||
|
|
||
| import { useParams, useRouter } from "next/navigation"; | ||
| import { useState } from "react"; | ||
|
|
||
| import Button from "@/shared/ui/Button"; | ||
| import Modal from "@/shared/ui/modal/Modal"; | ||
| import { RevisionCategorySection, RevisionCommentSection } from "@/widgets/instructor/revision"; | ||
| import { MAX_SELECTABLE_COUNT } from "@/widgets/instructor/revision/config/revision"; | ||
| import { | ||
| draftFilesData, | ||
| draftRevisionDetailData, | ||
| } from "@/widgets/instructor/revision/model/revision"; | ||
|
|
||
| const Page = () => { | ||
| const router = useRouter(); | ||
| const { commissionId } = useParams<{ commissionId: string }>(); | ||
| const draftRevisionDetail = draftRevisionDetailData.find( | ||
| detail => detail.commissionId === Number(commissionId), | ||
| ); | ||
| const draftFiles = draftFilesData.find(files => files.commissionId === Number(commissionId)); | ||
| const [selectedCategories, setSelectedCategories] = useState<string[]>([]); | ||
| const [comments, setComments] = useState<Record<string, string>>({}); | ||
| const [isFinalizeModalOpen, setIsFinalizeModalOpen] = useState(false); | ||
|
|
||
| const isFinalizeActive = selectedCategories.length === 0; | ||
| const isSubmitActive = | ||
| selectedCategories.length > 0 && | ||
| selectedCategories.every(category => (comments[category] ?? "").trim().length > 0); | ||
|
|
||
| const handleToggleCategory = (category: string) => { | ||
| setSelectedCategories(prev => { | ||
| if (prev.includes(category)) { | ||
| return prev.filter(selected => selected !== category); | ||
| } | ||
| if (prev.length >= MAX_SELECTABLE_COUNT) { | ||
| return prev; | ||
| } | ||
| return [...prev, category]; | ||
| }); | ||
| }; | ||
|
|
||
| const handleChangeComment = (category: string, value: string) => { | ||
| setComments(prev => ({ ...prev, [category]: value })); | ||
| }; | ||
|
|
||
| const handleCloseFinalizeModal = () => { | ||
| setIsFinalizeModalOpen(false); | ||
| }; | ||
|
|
||
| const handleConfirmFinalize = () => { | ||
| setIsFinalizeModalOpen(false); | ||
| router.push("/instructor"); | ||
| }; | ||
|
|
||
| if (draftRevisionDetail == null) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div className="mx-auto flex w-235 flex-col items-center pt-16 pb-19.5"> | ||
| <h1 className="text-title2-sb w-full py-4 pb-8 text-left text-black"> | ||
| {draftRevisionDetail.title} | ||
| </h1> | ||
| <div className="flex flex-col gap-10"> | ||
| <RevisionCategorySection | ||
| draftTitle={draftRevisionDetail.title} | ||
| designerComment={draftRevisionDetail.currentDraft.designerComment} | ||
| remainingRevisionCount={draftRevisionDetail.remainingRevisionCount} | ||
| maxRevisionCount={draftRevisionDetail.maxRevisionCount} | ||
| selectedCategories={selectedCategories} | ||
| onToggleCategory={handleToggleCategory} | ||
| fileUrls={draftFiles?.fileUrls ?? []} | ||
| /> | ||
| <RevisionCommentSection | ||
| comments={comments} | ||
| selectedCategories={selectedCategories} | ||
| onChangeComment={handleChangeComment} | ||
| /> | ||
| <div className="flex w-full flex-row justify-end gap-4"> | ||
| <Button | ||
| className="w-fit" | ||
| variant={isFinalizeActive ? "medium_primary" : "medium_disabled"} | ||
| onClick={isFinalizeActive ? () => setIsFinalizeModalOpen(true) : undefined} | ||
| > | ||
| 최종 시안으로 선택하기 | ||
| </Button> | ||
| <Button className="w-fit" variant={isSubmitActive ? "medium_primary" : "medium_disabled"}> | ||
| 수정사항 전달하기 | ||
| </Button> | ||
| </div> | ||
| </div> | ||
| <Modal | ||
| isOpen={isFinalizeModalOpen} | ||
| type="double" | ||
| title={"최종 시안으로 선택하시겠습니까?"} | ||
| description={"현재 디자인을 최종시안으로 선택하시면 \n더 이상 수정을 요청할 수 없습니다."} | ||
| confirmLabel="확인" | ||
| cancelLabel="취소" | ||
| onConfirm={handleConfirmFinalize} | ||
| onCancel={handleCloseFinalizeModal} | ||
| onClose={handleCloseFinalizeModal} | ||
| /> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default Page; | ||
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,75 @@ | ||
| import { type RefObject, useCallback, useEffect, useRef, useState } from "react"; | ||
|
|
||
| interface UseDragScrollbarProps { | ||
| scrollRef: RefObject<HTMLElement | null>; | ||
| } | ||
|
|
||
| export const useDragScrollbar = ({ scrollRef }: UseDragScrollbarProps) => { | ||
| const trackRef = useRef<HTMLDivElement>(null); | ||
| const [progress, setProgress] = useState(0); | ||
| const [isDragging, setIsDragging] = useState(false); | ||
|
|
||
| useEffect(() => { | ||
| const scrollEl = scrollRef.current; | ||
| if (!scrollEl || isDragging) return; | ||
|
|
||
| const syncProgress = () => { | ||
| const max = scrollEl.scrollWidth - scrollEl.clientWidth; | ||
| setProgress(max > 0 ? scrollEl.scrollLeft / max : 0); | ||
| }; | ||
|
|
||
| syncProgress(); | ||
| scrollEl.addEventListener("scroll", syncProgress); | ||
| return () => scrollEl.removeEventListener("scroll", syncProgress); | ||
| }, [scrollRef, isDragging]); | ||
|
|
||
| useEffect(() => { | ||
| const scrollEl = scrollRef.current; | ||
| if (!scrollEl) return; | ||
|
|
||
| const handleWheel = (e: WheelEvent) => { | ||
| e.preventDefault(); | ||
| scrollEl.scrollLeft += e.deltaY; | ||
| }; | ||
|
|
||
| scrollEl.addEventListener("wheel", handleWheel, { passive: false }); | ||
| return () => scrollEl.removeEventListener("wheel", handleWheel); | ||
| }, [scrollRef]); | ||
|
|
||
| const moveToClientX = useCallback( | ||
| (clientX: number) => { | ||
| const track = trackRef.current; | ||
| const scrollEl = scrollRef.current; | ||
| if (!track || !scrollEl) return; | ||
|
|
||
| const { left, width } = track.getBoundingClientRect(); | ||
| const ratio = Math.min(Math.max((clientX - left) / width, 0), 1); | ||
| const max = scrollEl.scrollWidth - scrollEl.clientWidth; | ||
|
|
||
| setProgress(ratio); | ||
| scrollEl.scrollLeft = ratio * max; | ||
| }, | ||
| [scrollRef], | ||
| ); | ||
|
|
||
| useEffect(() => { | ||
| if (!isDragging) return; | ||
|
|
||
| const handleMouseMove = (e: MouseEvent) => moveToClientX(e.clientX); | ||
| const handleMouseUp = () => setIsDragging(false); | ||
|
|
||
| document.addEventListener("mousemove", handleMouseMove); | ||
| document.addEventListener("mouseup", handleMouseUp); | ||
| return () => { | ||
| document.removeEventListener("mousemove", handleMouseMove); | ||
| document.removeEventListener("mouseup", handleMouseUp); | ||
| }; | ||
| }, [isDragging, moveToClientX]); | ||
|
|
||
| const handleTrackMouseDown = (clientX: number) => { | ||
| setIsDragging(true); | ||
| moveToClientX(clientX); | ||
| }; | ||
|
|
||
| return { trackRef, progress, handleTrackMouseDown }; | ||
| }; |
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,15 @@ | ||
| interface CommentCardProps { | ||
| title: string; | ||
| comment: string; | ||
| } | ||
|
|
||
| const CommentCard = ({ title, comment }: CommentCardProps) => { | ||
| return ( | ||
| <div className="bg-purple-5 border-purple-10 rounded-12 flex w-full flex-col gap-2.5 border px-6 py-4"> | ||
| <h1 className="text-main-main text-body1-sb">{title}</h1> | ||
| <h2 className="text-gray-80 text-body2-r scrollbar-hide h-16.5 overflow-y-auto">{comment}</h2> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default CommentCard; |
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,34 @@ | ||
| "use client"; | ||
|
|
||
| import type { RefObject } from "react"; | ||
|
|
||
| import { useDragScrollbar } from "@/shared/lib/hooks/useDragScrollbar"; | ||
| import { cn } from "@/shared/lib/utils/cn"; | ||
|
|
||
| interface DragScrollbarProps { | ||
| scrollRef: RefObject<HTMLElement | null>; | ||
| className?: string; | ||
| } | ||
|
|
||
| const DragScrollbar = ({ scrollRef, className }: DragScrollbarProps) => { | ||
| const { trackRef, progress, handleTrackMouseDown } = useDragScrollbar({ scrollRef }); | ||
| const percent = `${progress * 100}%`; | ||
|
|
||
| return ( | ||
| <div className={cn("flex h-12 items-center", className)}> | ||
| <div | ||
| ref={trackRef} | ||
| className="bg-purple-10 relative h-2 w-full cursor-pointer" | ||
| onMouseDown={e => handleTrackMouseDown(e.clientX)} | ||
| > | ||
| <div className="bg-purple-30 absolute inset-y-0 left-0" style={{ width: percent }} /> | ||
| <div | ||
| className="rounded-48 border-main-main absolute top-1/2 size-12 -translate-x-1/2 -translate-y-1/2 cursor-grab border-4 bg-white active:cursor-grabbing" | ||
| style={{ left: percent }} | ||
| /> | ||
| </div> | ||
| </div> | ||
| ); | ||
|
Comment on lines
+17
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift 접근성 개선 필요: 키보드 탐색 및 스크린리더 지원 커스텀 스크롤바가 마우스 인터랙션만 지원하고 키보드 접근성과 스크린리더 지원이 없습니다:
♿ 접근성 개선 제안 return (
<div className={cn("flex h-12 items-center", className)}>
<div
ref={trackRef}
className="bg-purple-10 relative h-2 w-full cursor-pointer"
onMouseDown={e => handleTrackMouseDown(e.clientX)}
+ role="scrollbar"
+ aria-orientation="horizontal"
+ aria-valuemin={0}
+ aria-valuemax={100}
+ aria-valuenow={Math.round(progress * 100)}
+ aria-label="수평 스크롤"
+ tabIndex={0}
+ onKeyDown={e => {
+ // 화살표 키로 스크롤 조정 로직 추가
+ }}
>
<div className="bg-purple-30 absolute inset-y-0 left-0" style={{ width: percent }} />
<div
className="rounded-48 border-main-main absolute top-1/2 size-12 -translate-x-1/2 -translate-y-1/2 cursor-grab border-4 bg-white active:cursor-grabbing"
style={{ left: percent }}
+ aria-hidden="true"
/>
</div>
</div>
);🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| export default DragScrollbar; | ||
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,40 @@ | ||
| import Image from "next/image"; | ||
|
|
||
| import { SearchIcon } from "@/shared/assets/icons"; | ||
| import { cn } from "@/shared/lib/utils/cn"; | ||
|
|
||
| interface ThumbnailProps { | ||
| src?: string; | ||
| alt?: string; | ||
| className?: string; | ||
| onDetailClick?: () => void; | ||
| } | ||
|
|
||
| const Thumbnail = ({ | ||
| src = "/images/thumbnail_mock.jpg", | ||
| alt = "썸네일", | ||
| className, | ||
| onDetailClick, | ||
| }: ThumbnailProps) => { | ||
| return ( | ||
| <div | ||
| className={cn( | ||
| "rounded-12 bg-gray-20 group relative h-full w-full overflow-hidden", | ||
| className, | ||
| )} | ||
| > | ||
| <Image src={src} alt={alt} fill sizes="250px" loading="eager" className="object-cover" /> | ||
| <div className="bg-overlay-hover backdrop-blur-hover absolute inset-0 opacity-0 transition-opacity group-hover:opacity-100" /> | ||
| <button | ||
| type="button" | ||
| onClick={onDetailClick} | ||
| className="backdrop-blur-button text-body2-m rounded-12 absolute top-1/2 left-1/2 flex -translate-x-1/2 -translate-y-1/2 cursor-pointer flex-row items-center gap-2.5 bg-white/18 px-4 py-2 text-white opacity-0 transition-opacity group-hover:opacity-100" | ||
| > | ||
| 자세히 보기 | ||
| <SearchIcon className="size-5" /> | ||
| </button> | ||
| </div> | ||
| ); | ||
| }; | ||
|
|
||
| export default Thumbnail; |
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"수정사항 전달하기" 버튼에 onClick 핸들러가 누락되었습니다.
isSubmitActive로직으로 버튼 활성화 상태는 제어하고 있지만, 실제로 수정사항을 제출하는 onClick 핸들러가 구현되지 않았습니다. 사용자가 카테고리를 선택하고 코멘트를 입력한 후 이 버튼을 클릭해도 아무 동작이 발생하지 않습니다.🔧 onClick 핸들러 추가 예시
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
추후 로직 연결 예정
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@waldls, 이해했습니다. 나중에 API 연결 시isSubmitActive조건 체크와 함께 핸들러를 붙여주시면 됩니다.추후 작업 추적을 위해 GitHub 이슈를 생성해 드릴까요?
🧠 Learnings used