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
8 changes: 7 additions & 1 deletion features/project/components/project-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,13 @@ const ProjectCardHeader = ({ isLeader, projectName, projectStatus, startDate, on
</time>
{isLeader ? (
// 카드 전체가 상세 페이지 Link이므로 드롭다운 클릭이 네비게이션으로 전파되지 않도록 막는다
<div onClick={(e) => e.stopPropagation()}>
<div
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
onPointerDown={(e) => e.stopPropagation()}
>
<Dropdown.Root>
<Dropdown.Trigger asChild>
<IconButton
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
'use client';

import { CopyIcon, RoundCheckIcon } from '@/shared/assets/icons';
import Link from 'next/link';

import { ArrowLeftIcon, CopyIcon, RoundCheckIcon } from '@/shared/assets/icons';
import { Button } from '@/shared/components/button';
import { CopyFeedbackTooltip } from '@/shared/components/copy-feedback-tooltip';
import { useCopyLinkFeedback } from '@/shared/hooks/use-copy-link-feedback';
import { cn } from '@/shared/lib/cn';

import type { ProjectStatus } from '../../project.types';

Expand All @@ -20,6 +23,10 @@ interface Props {

export const ProjectDetailHeading = ({ projectId, projectStatus, isLeader, projectLink, inviterName }: Props) => {
const isInProgress = projectStatus === 'IN_PROGRESS';
const isCompleted = projectStatus === 'COMPLETED';
// 팀 확정 후에도 초대 링크 복사·비활성 확정 버튼을 노출한다 (Figma 방장 확정 완료 시점).
const showLeaderActions = isLeader && (isInProgress || isCompleted);
const isConfirmDisabled = !isInProgress;
const { open, message, close, copyLink } = useCopyLinkFeedback();
const { mutate: confirmMembers, isPending: isConfirming } = useConfirmProjectMembers(projectId);

Expand All @@ -28,20 +35,68 @@ export const ProjectDetailHeading = ({ projectId, projectStatus, isLeader, proje
};

return (
<header className="border-border-gray-light flex items-end justify-between border-b-[3px] pt-15.5 pb-8">
<div className="flex flex-col gap-2">
<h1 className="text-heading-lg text-text-basic font-bold">프로젝트 상세</h1>
<p className="text-body-base text-text-subtle">프로젝트 정보와 팀원을 확인하고, 필요하면 수정할 수 있어요.</p>
<header
className={cn(
'lg:border-border-gray-light flex flex-col pt-8 pb-5 lg:gap-0 lg:border-b-[3px] lg:pt-15.5 lg:pb-8',
// 방장: 액션 버튼 행 아래 구분선. 팀원: 모바일은 제목만(Figma), 데스크톱은 구분선 유지.
showLeaderActions && 'border-border-gray-light gap-5 border-b-[3px]',
)}
>
<div className="flex items-end justify-between">
<div className="flex min-w-0 flex-col gap-2">
<div className="flex items-center gap-1 lg:block">
<Link
href="/projects"
aria-label="프로젝트 관리로 돌아가기"
className="text-text-basic inline-flex size-6 shrink-0 items-center justify-center lg:hidden"
>
<ArrowLeftIcon aria-hidden className="size-6" />
</Link>
<h1 className="text-heading-sm lg:text-heading-lg text-text-basic font-bold">프로젝트 상세</h1>
</div>
<p className="text-body-base text-text-subtle hidden lg:block">
프로젝트 정보와 팀원을 확인하고, 필요하면 수정할 수 있어요.
</p>
</div>

{showLeaderActions ? (
<div className="hidden items-center gap-2 lg:flex">
<div className="relative inline-flex">
<Button
type="button"
variant="secondary"
size="medium"
leftIcon={<CopyIcon aria-hidden className="size-4" />}
className={cn(open && 'bg-button-secondary-fill-pressed')}
onClick={handleCopyInviteLink}
>
초대 링크 복사
</Button>
<CopyFeedbackTooltip open={open} onClose={close} message={message} />
</div>
<Button
type="button"
variant="primary"
size="medium"
leftIcon={<RoundCheckIcon aria-hidden className="size-4" />}
disabled={isConfirmDisabled || isConfirming}
onClick={() => confirmMembers()}
>
우리 팀 확정하기
</Button>
</div>
) : null}
</div>

{isLeader && isInProgress ? (
<div className="flex items-center gap-2">
<div className="relative inline-flex">
{showLeaderActions ? (
<div className="flex items-start gap-2 lg:hidden">
<div className="relative min-w-0 flex-1">
<Button
type="button"
variant="secondary"
size="medium"
leftIcon={<CopyIcon aria-hidden className="size-4" />}
className={cn('h-11 w-full justify-center', open && 'bg-button-secondary-fill-pressed')}
onClick={handleCopyInviteLink}
>
초대 링크 복사
Expand All @@ -53,7 +108,8 @@ export const ProjectDetailHeading = ({ projectId, projectStatus, isLeader, proje
variant="primary"
size="medium"
leftIcon={<RoundCheckIcon aria-hidden className="size-4" />}
disabled={isConfirming}
className="h-11 min-w-0 flex-1 justify-center"
disabled={isConfirmDisabled || isConfirming}
onClick={() => confirmMembers()}
>
우리 팀 확정하기
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const ProjectDetailPageContent = ({ projectId }: Props) => {
);

return (
<PageContainer className="mb-20 flex flex-col gap-8">
<PageContainer className="mb-20 flex flex-col gap-6 lg:gap-8">
<ProjectDetailHeading
projectId={projectId}
projectStatus={project.projectStatus}
Expand Down
70 changes: 33 additions & 37 deletions features/project/components/project-detail/project-info-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
import { useEffect, useState } from 'react';
import { Controller } from 'react-hook-form';

import { TrashIcon } from '@/shared/assets/icons';
import { Button } from '@/shared/components/button';
import { DatePicker } from '@/shared/components/date-picker';
import { ErrorMessage } from '@/shared/components/error-message';
import { FileInput, useImagePreview } from '@/shared/components/file-input';
import { Label } from '@/shared/components/label';
import { SectionCard, SectionInfoRow } from '@/shared/components/section-card';
import { Select } from '@/shared/components/select/select';
import { TextField } from '@/shared/components/text-field';
import { cn } from '@/shared/lib/cn';
import { formatIsoDateToDots } from '@/shared/lib/format-date';

import type { ProjectResponse } from '../../project.types';
Expand Down Expand Up @@ -55,10 +55,11 @@
onOpenChange: setIsEditing,
});

// 팀 확정(IN_PROGRESS 종료) 이후에는 프로젝트 정보를 수정할 수 없으므로 수정 버튼을 숨긴다.
// 팀 확정 후에는 수정 불가. 방장에게는 disabled 수정 버튼을 노출한다 (Figma).
const isEditable = isLeader && project.projectStatus === 'IN_PROGRESS';
const showEditAction = isLeader && (project.projectStatus === 'IN_PROGRESS' || project.projectStatus === 'COMPLETED');

const action = !isEditable ? null : isEditing ? (
const action = !showEditAction ? null : isEditing ? (
<div className="flex gap-2">
<Button type="button" variant="tertiary" onClick={() => handleOpenChange(false)} disabled={isSubmitting}>
취소
Expand All @@ -77,8 +78,10 @@
<Button
type="button"
variant="tertiary"
size="small"
disabled={!isEditable}
onClick={() => setIsEditing(true)}
className="border-border-gray-dark text-text-subtle border"
className={cn(!isEditable ? undefined : 'border-border-gray-dark text-text-subtle border')}
>
수정
</Button>
Expand Down Expand Up @@ -107,12 +110,12 @@
<span className="text-text-basic text-body-base">{formatDateRange(project.startDate, project.endDate)}</span>
</SectionInfoRow>
<SectionInfoRow label="협업 인증 사진" align="start">
<div className="relative size-25 overflow-hidden rounded-2xl">
<div className="relative size-20 overflow-hidden rounded-2xl lg:size-25">
<Image
src={project.projectImage ?? DEFAULT_PROJECT_IMAGE}
alt={`${project.projectName} 협업 인증 사진`}
fill
sizes="100px"
sizes="(min-width: 1024px) 100px, 80px"
className="object-cover"
/>
</div>
Expand Down Expand Up @@ -209,7 +212,7 @@
/>
)}
/>
<span className="text-text-subtle text-body-base">-</span>
<span className="text-text-subtle text-body-base">~</span>
<Controller
control={control}
name="endDate"
Expand Down Expand Up @@ -248,40 +251,33 @@
</SectionInfoRow>

<SectionInfoRow label="URL" align="start">
<div className="grid grid-cols-[1fr_120px] items-start gap-2">
<Controller
control={control}
name="projectUrl"
render={({ field }) => (
<Controller
control={control}
name="projectUrl"
render={({ field }) => (
<div className="flex items-start gap-2">
<TextField
label="URL"
placeholder="https://"
errorMessage={errors.projectUrl?.message}
disabled={isSubmitting}
className="[&>label]:sr-only"
className="min-w-0 flex-1 [&>label]:sr-only"
{...field}
value={field.value ?? ''}
/>
)}
/>
<div className="flex flex-col gap-2">
<Label className="sr-only">URL 유형</Label>
<Controller
control={control}
name="projectUrlType"
render={({ field }) => (
<Select.Root value={field.value ?? 'LINK'} onValueChange={field.onChange} disabled={isSubmitting}>
<Select.Trigger aria-label="URL 유형">
<Select.Value />
</Select.Trigger>
<Select.Content className="w-(--radix-select-trigger-width)">
<Select.Item value="LINK">Link</Select.Item>
</Select.Content>
</Select.Root>
)}
/>
</div>
</div>
<Button
type="button"
variant="tertiary"
size="medium"
aria-label="URL 지우기"
disabled={isSubmitting || !field.value}
className="bg-button-tertiary-fill-hover h-12 shrink-0 px-2.5"
leftIcon={<TrashIcon className="size-5" aria-hidden />}
onClick={() => field.onChange('')}
/>
</div>
)}
/>
</SectionInfoRow>

{errors.root?.message ? <ErrorMessage className="static">{errors.root.message}</ErrorMessage> : null}
Expand All @@ -306,11 +302,11 @@
}, [syncPreview, value]);

return (
<div className="flex items-start gap-6">
<div className="flex items-start gap-3 lg:gap-6">
<img

Check warning on line 306 in features/project/components/project-detail/project-info-card.tsx

View workflow job for this annotation

GitHub Actions / build-and-test

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={imageSrc}
alt={`${project.projectName} 협업 인증 사진 미리보기`}
className="size-25 shrink-0 rounded-2xl object-cover"
className="size-20 shrink-0 rounded-2xl object-cover lg:size-25"
/>
<div className="flex min-w-0 flex-col gap-2">
<FileInput
Expand All @@ -321,7 +317,7 @@
disabled={disabled}
className="max-w-full"
/>
<p className="text-body-sm text-text-subtler whitespace-pre-line">{IMAGE_GUIDE_TEXT}</p>
<p className="text-detail-xs lg:text-body-sm text-text-subtler whitespace-pre-line">{IMAGE_GUIDE_TEXT}</p>
{errorMessage ? <ErrorMessage className="static">{errorMessage}</ErrorMessage> : null}
</div>
</div>
Expand Down
Loading
Loading