Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion src/components/DetailView/Comment/CommentInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ const CommentInput = ({ onAdd }: CommentIntputProps) => {

return (
<div
className="flex flex-end items-end gap-[0.8rem] w-full h-[13rem] p-[2.4rem] bg-gray-100 rounded-lg"
className="absolute bottom-0 z-20 w-full flex items-end gap-[0.8rem] h-[13rem] p-[2.4rem] bg-gray-100 rounded-lg"
style={{
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.10), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
}}
>
{/* 댓글 입력창 영역 */}
<textarea
name="commentInput"
id="commentInput"
value={comment}
onChange={handleChange}
placeholder="댓글을 작성하세요."
Expand Down
6 changes: 3 additions & 3 deletions src/components/DetailView/Comment/CommentItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ const CommentItem = ({ author, content, createAt }: CommentItemProps) => {

<div className="flex flex-col gap-[0.8rem]">
{/* 작성자 정보 */}
<div className="flex gap-[0.8rem]">
<div className="flex items-center gap-[0.8rem]">
{/* 작성자 이름 */}
<span className="font-body-b text-gray-600">{author}</span>
<span className="font-body-b text-gray-600 truncate">{author}</span>
{/* 작성 시간 */}
<span className="font-small-r text-gray-500">{createAt.toLocaleString()}</span>
<span className="font-small-r text-gray-500 truncate">{createAt.toLocaleString()}</span>
</div>

{/* 댓글 내용 */}
Expand Down
45 changes: 29 additions & 16 deletions src/components/DetailView/Comment/CommentSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,45 @@ const CommentSection = () => {
};

return (
<div className="flex flex-col gap-[3.2rem] w-full h-[41rem]">
<div className="relative flex flex-col flex-1 gap-[3.2rem] w-full min-h-0">
{/* 댓글 목록 헤더 */}
<div className="flex gap-[0.8rem]">
<div className="font-title-sub-r text-gray-600">댓글</div>
{/* 댓글 개수를 숫자로 렌더링 */}
<div className="font-title-sub-r text-gray-500">{comments.length}</div>
</div>

<div className="flex flex-col gap-[1.6rem] w-full h-[35rem]">
<div className="flex flex-col w-full max-h-full overflow-y-auto basic-scroll">
{/* 댓글 목록 */}
<div className="flex flex-col gap-[2.4rem] w-full h-full overflow-y-auto basic-scroll">
{comments.length === 0 ? (
<div className="flex items-center justify-center w-full h-full">
{/* 댓글 개수가 0개일 때: 댓글 목록의 초기 문구를 띄우도록 */}
<div className="font-body-r text-gray-600">댓글을 작성하세요.</div>
{comments.length === 0 ? (
// 댓글 개수가 0개일 때: 댓글 목록의 초기 문구를 띄우도록
<div className="flex items-center justify-center w-full">
<div className="font-body-r text-gray-600">댓글을 작성하세요.</div>
</div>
) : (
// 댓글이 있을 때: 댓글 목록 렌더링
<>
<div className="absolute bottom-[14.6rem] w-full z-10 pr-[1.2rem]">
{/* 댓글 목록 위에 옅은 흰색 그라디언트 처리 */}
<div className="w-full h-[6.4rem] bg-gradient-to-b from-white/0 to-white pointer-events-none" />
</div>
) : (
comments.map(({ author, content, createAt }) => (
<CommentItem author={author} content={content} createAt={createAt} />
))
)}
</div>

{/* 댓글 입력창 */}
<CommentInput onAdd={handleAddComment} />
{/* 댓글을 순서대로 목록에 추가 */}
<div className="flex flex-col gap-y-[2.4rem] mb-[14.6rem] w-full overflow-y-scroll basic-scroll">
{comments.map(({ author, content, createAt }) => (
<CommentItem
key={`${createAt.getTime()}-${author}`} // Date를 밀리초 숫자로 변환
author={author}
content={content}
createAt={createAt}
/>
))}
</div>
</>
)}
</div>

{/* 댓글 입력창 */}
<CommentInput onAdd={handleAddComment} />
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/components/DetailView/CompletionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const CompletionButton = ({ isTitleFilled, isCompleted, onToggle }: CompletionBu
const textColor = isDisabled ? 'text-gray-400' : 'text-gray-100';

return (
<div className="flex items-end justify-end">
<div className="flex items-end justify-end fixed bottom-[5.3rem] right-[6.4rem] z-20">
<button
onClick={isDisabled ? undefined : onToggle}
disabled={isDisabled}
Expand Down
4 changes: 3 additions & 1 deletion src/components/DetailView/DetailTextEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ interface DetailTextEditorProps {
const DetailTextEditor = ({ isEditable }: DetailTextEditorProps) => {
return (
<textarea
name="detailTextEditor"
id="detailTextEditor"
disabled={!isEditable}
className="w-full h-full font-body-r placeholder-gray-400 text-gray-600 overflow-y-scroll basic-scroll resize-none focus:outline-none pr-4 ${!isEditable ? 'cursor-not-allowed text-gray-600' : ''}"
className="w-full flex-1 font-body-r placeholder-gray-400 text-gray-600 overflow-y-scroll basic-scroll resize-none focus:outline-none pr-4 ${!isEditable ? 'cursor-not-allowed text-gray-600' : ''}"
placeholder="상세 설명 추가"
></textarea>
);
Expand Down
7 changes: 7 additions & 0 deletions src/components/DetailView/DetailTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,17 @@ const DetailTitle = ({ defaultTitle, title, setTitle, isEditable }: DetailTitleP

return (
<textarea
name="detailTitle"
id="detailTitle"
ref={textarea}
value={title}
rows={1}
onChange={handleChange}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault(); // 엔터 키를 눌러 직접 줄바꿈하는 것 방지
}
}}
disabled={!isEditable}
// 각 페이지별 placeholder를 서로 다르게 할 수 있도록 defaultTitle로 처리
placeholder={defaultTitle}
Expand Down
23 changes: 11 additions & 12 deletions src/components/DetailView/PropertyItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
* PropertyItem.tsx
* 상세페이지 내 속성 항목 컴포넌트
*
* @todo
* - 화면 가로 길이 줄여도 줄어들지 않게 처리
*
* @description
* - 속성 항목 수정은 상세페이지 내의 작성 완료 버튼 클릭 여부에 영향을 받지 않도록 구현.
* (즉, 속성은 언제나 수정 가능함)
* - 기한 드롭다운은 이 컴포넌트로 관리하지 않음 (별도로 필요한 상세페이지에서 구현)
*
* @todo
* - 현재는 '기한' 속성과 '이슈' 속성의 드롭다운은 이 컴포넌트에서 관리하지 않음.
* - 추후 더 나은 컴포넌트 연결 방식 고민해보고 리팩토링 예정.
*/

import { useState } from 'react';
Expand All @@ -26,8 +26,8 @@ const PropertyItem = ({ defaultValue, options, iconMap, getColor }: PropertyItem
const initialValue = defaultValue && iconMap && iconMap[defaultValue] ? defaultValue : options[0];

const [value, setValue] = useState(defaultValue);
const [isOpen, setIsOpen] = useState(false);
const [icon, setIcon] = useState(iconMap ? iconMap[initialValue] : undefined);
const [isOpen, setIsOpen] = useState(false);

const handleSelect = (option: string) => {
setValue(option);
Expand All @@ -37,7 +37,8 @@ const PropertyItem = ({ defaultValue, options, iconMap, getColor }: PropertyItem

return (
<div
className={`flex w-full h-[3.2rem] px-[0.5rem] rounded-md items-center gap-[0.8rem] mb-[1.6rem] whitespace-nowrap hover:bg-gray-200`}
onClick={() => setIsOpen(!isOpen)}
className={`flex w-full h-[3.2rem] px-[0.5rem] rounded-md items-center gap-[0.8rem] mb-[1.6rem] whitespace-nowrap hover:bg-gray-200 cursor-pointer`}
>
{/* 속성 아이콘 */}
{getColor ? (
Expand All @@ -54,13 +55,11 @@ const PropertyItem = ({ defaultValue, options, iconMap, getColor }: PropertyItem
)}

{/* 속성 이름 */}
<div className={`flex relative cursor-pointer`}>
<span className="flex items-center" onClick={() => setIsOpen(!isOpen)}>
{/* 속성 항목명 */}
<div className="font-body-r text-gray-600">{value}</div>
</span>
<div className={`flex relative`}>
{/* 속성 항목명 */}
<p className="font-body-r text-gray-600 max-w-[27.4rem] truncate">{value}</p>

{/* 속성 항목명을 눌러 드롭다운 오픈 */}
{/* 드롭다운 오픈 */}
{isOpen && (
<Dropdown
value={value}
Expand Down
1 change: 1 addition & 0 deletions src/components/Dropdown/ArrowDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const ArrowDropdown = ({
style={{ boxShadow: '0px 4px 12px 0px rgba(0, 0, 0, 0.15)' }}
className={`absolute z-30 top-0 left-0 flex flex-col w-[27.4rem]
border border-gray-400 bg-white rounded-[0.4rem] ${className}`}
onClick={(e) => e.stopPropagation()}
>
<div
className={`flex border-b border-gray-200 justify-between items-center py-[0.4rem] ps-[1.2rem] pe-[0.8rem]`}
Expand Down
87 changes: 73 additions & 14 deletions src/pages/goal/GoalDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,36 @@ import pr2 from '../../assets/icons/pr-2-sm.svg';
import pr3 from '../../assets/icons/pr-3-sm.svg';
import pr4 from '../../assets/icons/pr-4-sm.svg';
import IcProfile from '../../assets/icons/user-circle-sm.svg';
// import IcDate from '../../assets/icons/date-lg.svg';
// import IcIssue from '../../assets/icons/issue.svg';
import IcCalendar from '../../assets/icons/date-lg.svg';
import IcIssue from '../../assets/icons/issue.svg';

import { getStatusColor } from '../../utils/listItemUtils';
import { statusLabelToCode } from '../../types/detailitem';
import CommentSection from '../../components/DetailView/Comment/CommentSection';
import CalendarDropdown from '../../components/Calendar/CalendarDropdown';
import { useDropdownActions, useDropdownInfo } from '../../hooks/useDropdown';
import { formatDateDot } from '../../utils/formatDate';
import ArrowDropdown from '../../components/Dropdown/ArrowDropdown';

const GoalDetail = () => {
const [title, setTitle] = useState('');
const [isCompleted, setIsCompleted] = useState(false);

// '기한' 속성의 달력 드롭다운: 시작일, 종료일 2개를 저장
const [selectedDate, setSelectedDate] = useState<[Date | null, Date | null]>([null, null]);

const [option, setOption] = useState<string>('이슈');
const { isOpen, content } = useDropdownInfo(); // 현재 드롭다운의 열림 여부와 내용 가져옴
const { openDropdown, closeDropdown } = useDropdownActions();

// '기한' 속성의 텍스트(시작일, 종료일) 결정하는 함수
const getDisplayText = () => {
const [start, end] = selectedDate;
if (start && end) return `${formatDateDot(start)} - ${formatDateDot(end)}`; // 시작일과 종료일 둘 다 있을 경우
if (start || end) return formatDateDot(start ?? end!); // 날짜 하나만 선택된 경우
return '기한'; // 날짜 선택 안 된 경우: default로 '기한' 글씨가 그대로 보이도록
};

// '우선순위' 속성 아이콘 매핑
const priorityIconMap = {
우선순위: pr3,
Expand Down Expand Up @@ -58,7 +77,7 @@ const GoalDetail = () => {
{/* 상세페이지 메인 */}
<div className="flex px-[3.2rem] gap-[8.8rem] w-full h-full">
{/* 상세페이지 좌측 영역 - 제목 & 상세설명 & 댓글 */}
<div className="flex flex-col gap-[3.2rem] w-[calc(100%-33rem)]">
<div className="flex flex-col gap-[3.2rem] w-[calc(100%-33rem)] h-full">
{/* 상세페이지 제목 */}
<DetailTitle
defaultTitle="목표를 생성하세요"
Expand Down Expand Up @@ -111,19 +130,59 @@ const GoalDetail = () => {
</div>

{/* (4) 기한 */}
{/* <PropertyItem defaultValue="기한" iconMap={{ 기한: dateIconMap }} /> */}
<div
onClick={(e) => {
e.stopPropagation();
openDropdown({ name: 'date' });
}}
className="flex w-full h-[3.2rem] px-[0.5rem] rounded-md items-center gap-[0.8rem] mb-[1.6rem] whitespace-nowrap hover:bg-gray-200 cursor-pointer"
>
{/* '기한' 속성 아이콘 */}
<img src={IcCalendar} alt="date" />
<div className="relative">
{/* '기한' 항목명 - 날짜 설정하면 반영됨 */}
<span className={`font-body-r text-gray-600`}>{getDisplayText()}</span>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

p2: 기한명 서식 2025.07.28 이 아닌 25.07.08 입니다

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

넵 감사합니다 수정 반영하겠습니다~

{/* 달력 드롭다운 오픈 */}
{isOpen && content?.name === 'date' && (
<CalendarDropdown
selectedDate={selectedDate}
onSelect={(date) => setSelectedDate(date)}
/>
)}
</div>
</div>

{/* (5) 이슈 */}
{/**
* @todo
* - 이슈 많아질 경우 '외 n개'로 축약되게 하기
*/}
{/*
<PropertyItem
defaultValue="이슈"
iconMap={{ 이슈: issueIconMap }}
/>
*/}
<div
onClick={(e) => {
e.stopPropagation();
openDropdown({ name: '이슈' });
}}
className={`flex w-full h-[3.2rem] px-[0.5rem] rounded-md items-center gap-[0.8rem] mb-[1.6rem] whitespace-nowrap hover:bg-gray-200 cursor-pointer`}
>
{/* 속성 아이콘 */}
<img src={IcIssue} alt="이슈" />

{/* 속성 이름 */}
<div className="flex relative">
{/* 속성 항목명 */}
<p className="font-body-r text-gray-600 max-w-[27.4rem] truncate">{option}</p>

{/* 드롭다운 오픈 */}
{isOpen && content?.name === '이슈' && (
<ArrowDropdown
defaultValue={'이슈'}
options={[
'기능 정의: 구현할 핵심 기능과 어쩌구 저쩌구 텍스트가 길어지면 이렇게 표시',
'와이어프레임 디자인',
'컴포넌트 정리',
]}
onSelect={(value: string) => setOption(value)}
onClose={closeDropdown}
/>
)}
Comment on lines +172 to +183

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P1: 드롭다운이 정상적으로 닫히지 않는 문제는 e.stopPropagation() 처리가 누락되어 발생한 것으로 확인되었습니다...
ArrowDropdown.tsx 상위 div 태그에 아래와 같이 onClick 이벤트에 e.stopPropagation()을 추가해주시면 문제를 해결하실 수 있습니다. 죄송합니다 😢

 <div
      ref={dropdownRef}
      style={{ boxShadow: '0px 4px 12px 0px rgba(0, 0, 0, 0.15)' }}
      className={`absolute z-30 top-0 left-0 flex flex-col w-[27.4rem]
      border border-gray-400 bg-white rounded-[0.4rem] ${className}`}
      onClick={(e) => e.stopPropagation()}
    >

@HiJuwon HiJuwon Aug 1, 2025

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

이거는 아래 내용 보시면, 해당 속성 항목이 차지하는 전체 영역을 눌렀을 때 드롭다운이 열리게 하는 방식으로 UX를 개선하고자 e.stopPropagation() 처리도 속성 항목의 최상위 div에 적용을 했는데요,

바로 직속 상위 div 태그에 이벤트 처리하지 않고, 아래와 같은 방식으로 처리하면 해당 이벤트 처리가 안 먹히는 건가 싶어서 해당 e.stopPropagation() 처리만 최상위 div 태그가 아닌 바로 직속 상위 div 태그에 넣어봤는데, 이렇게 하면 문제는 해결되지만 대신에 바로 직속 상위 div 태그인 속성 이름을 클릭하면 드롭다운이 열리지 않는 문제가 생깁니다...

{/* (5) 이슈 */}
<div
  onClick={(e) => {
    e.stopPropagation();
    openDropdown({ name: '이슈' });
  }}
  className={`flex w-full h-[3.2rem] px-[0.5rem] rounded-md items-center gap-[0.8rem] mb-[1.6rem] whitespace-nowrap hover:bg-gray-200 cursor-pointer`}
>
  {/* 속성 아이콘 */}
  <img src={IcIssue} alt="이슈" />

  {/* 속성 이름 */}
  <div className="flex relative">
    {/* 속성 항목명 */}
    <p className="font-body-r text-gray-600 max-w-[27.4rem] truncate">{option}</p>

    {/* 드롭다운 오픈 */}
    {isOpen && content?.name === '이슈' && (
      <ArrowDropdown
        defaultValue={'이슈'}
        options={[
          '기능 정의: 구현할 핵심 기능과 어쩌구 저쩌구 텍스트가 길어지면 이렇게 표시',
          '와이어프레임 디자인',
          '컴포넌트 정리',
        ]}
        onSelect={(value: string) => setOption(value)}
        onClose={closeDropdown}
      />
    )}
  </div>
</div>

이 부분에 대해서도 한번만 봐주시면 좋을 것 같아요.... ㅠㅠ 어떻게 해야할지 모르겠습니다 @gaeulzzang

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

image GoalDetail.tsx 코드는 그대로 두고 ArrowDropdown.tsx 코드로 가서 제가 마우스로 드래그한 부분만 추가해주시면 됩니답

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

아아 넵 해결했습니다!!!

</div>
</div>
</div>
</div>

Expand Down
15 changes: 13 additions & 2 deletions src/pages/goal/GoalHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@ import {
} from '../../types/testDummy';
import type { GoalFilter, GroupedGoal } from '../../types/goal';
import { getSortedGrouped } from '../../utils/listGroupSortUtils';
import { useNavigate } from 'react-router-dom';

const FILTER_OPTIONS: ItemFilter[] = ['상태', '우선순위', '담당자'] as const;

const GoalHome = () => {
const { isOpen, content } = useDropdownInfo();
const { openDropdown, closeDropdown } = useDropdownActions();
const [filter, setFilter] = useState<ItemFilter>('상태');
const navigate = useNavigate();

const handleClick = () => {
navigate(':goalId');
};

// filter 변경마다 다른 데이터 선택 -> 추후 새로운 데이터 불러오도록
const dummyGoalGroups = useMemo<GoalFilter[]>(() => {
Expand Down Expand Up @@ -122,8 +128,13 @@ const GoalHome = () => {
</div>
<div className="text-gray-500 ml-[0.8rem]">{items.length}</div>
</div>
{/* TODO : 추가 버튼 라우터 연결 */}
<img src={PlusIcon} className="inline-block w-[2.4rem] h-[2.4rem]" alt="" />
{/* 추가 버튼 */}
<img
src={PlusIcon}
className="inline-block w-[2.4rem] h-[2.4rem]"
alt=""
onClick={handleClick}
/>
</div>
{/* 각 유형 별 요소 */}
{items.map((goal) => (
Expand Down
4 changes: 2 additions & 2 deletions src/utils/formatDate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ export const formatDate = (date: Date) => {
};

export const formatDateDot = (date: Date): string => {
const yyyy = date.getFullYear();
const yy = date.getFullYear().toString().slice(-2); // 연도 뒤 2자리만 사용
const mm = String(date.getMonth() + 1).padStart(2, '0');
const dd = String(date.getDate()).padStart(2, '0');
return `${yyyy}.${mm}.${dd}`;
return `${yy}.${mm}.${dd}`;
};