feat(project): 계약범위 글/그림 입력칸 + 인턴/직책/초과근무/로고/로그아웃 개선분
All checks were successful
build-and-push / build (push) Successful in 30s

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
theorose49 2026-06-28 11:23:38 +09:00
parent e3b5a874b3
commit 581fd7a19f
5 changed files with 18 additions and 17 deletions

View File

@ -119,6 +119,7 @@ export const STAGE_KIND_LABELS: Record<string, string> = {
final: "잔금", final: "잔금",
}; };
// 인센티브 BE/non-BE 라벨 (프로젝트 계약범위와 무관 — 그쪽은 자유 텍스트)
export const SCOPE_LABELS: Record<string, string> = { export const SCOPE_LABELS: Record<string, string> = {
be: "BE", be: "BE",
non_be: "non-BE", non_be: "non-BE",

View File

@ -18,7 +18,7 @@ import {
import { Gantt } from "@/components/Gantt"; import { Gantt } from "@/components/Gantt";
import { Kanban } from "@/components/Kanban"; import { Kanban } from "@/components/Kanban";
import { import {
formatDate, formatWon, formatSize, PROJECT_STATUS_META, SCOPE_LABELS, LANE_LABELS, classNames, formatDate, formatWon, formatSize, PROJECT_STATUS_META, LANE_LABELS, classNames,
} from "@/lib/format"; } from "@/lib/format";
import type { Lane, PaymentSplit, Project, ProjectTask } from "@/types"; import type { Lane, PaymentSplit, Project, ProjectTask } from "@/types";
@ -46,7 +46,7 @@ export function ProjectDetailPage() {
<Link to="/projects" className="inline-flex items-center gap-1 text-sm text-ink-secondary hover:text-ink mb-3"><ArrowLeft size={15} /> </Link> <Link to="/projects" className="inline-flex items-center gap-1 text-sm text-ink-secondary hover:text-ink mb-3"><ArrowLeft size={15} /> </Link>
<PageHeader <PageHeader
title={<span className="flex items-center gap-2">{p.name} {m && <Badge label={m.label} fg={m.fg} bg={m.bg} dot size="sm" />}</span>} title={<span className="flex items-center gap-2">{p.name} {m && <Badge label={m.label} fg={m.fg} bg={m.bg} dot size="sm" />}</span>}
description={`${p.companyName} · ${p.productName} ${p.versionName} · ${p.consultingType} · ${p.country} · ${SCOPE_LABELS[p.scope] ?? p.scope}`} description={`${p.companyName} · ${p.productName} ${p.versionName} · ${p.consultingType} · ${p.country}`}
/> />
<Card> <Card>
<div className="px-3 pt-2"><Tabs tabs={tabs} active={tab} onChange={setTab} /></div> <div className="px-3 pt-2"><Tabs tabs={tabs} active={tab} onChange={setTab} /></div>
@ -77,7 +77,8 @@ function Overview({ project: p }: { project: Project }) {
<div> <div>
<Row label="컨설팅 종류" value={p.consultingType} /> <Row label="컨설팅 종류" value={p.consultingType} />
<Row label="제출 국가" value={p.country} /> <Row label="제출 국가" value={p.country} />
<Row label="계약 범위" value={SCOPE_LABELS[p.scope] ?? p.scope} /> <Row label="계약 범위(글)" value={p.scopeText} />
<Row label="계약 범위(그림)" value={p.scopeGraphic} />
<Row label="PM" value={p.pmEmail} /> <Row label="PM" value={p.pmEmail} />
</div> </div>
<div> <div>

View File

@ -10,7 +10,7 @@ import {
Card, Button, Badge, PageHeader, Modal, Field, Input, Select, Textarea, Card, Button, Badge, PageHeader, Modal, Field, Input, Select, Textarea,
EmptyState, LoadingState, EmptyState, LoadingState,
} from "@/components/ui"; } from "@/components/ui";
import { formatDate, PROJECT_STATUS_META, SCOPE_LABELS } from "@/lib/format"; import { formatDate, PROJECT_STATUS_META } from "@/lib/format";
// 나의 업무 > 프로젝트: 본인이 참여한 프로젝트만 보는 read-only 뷰 (관리자·유저 동일). // 나의 업무 > 프로젝트: 본인이 참여한 프로젝트만 보는 read-only 뷰 (관리자·유저 동일).
// 생성/관리는 관리자 전용 페이지(/admin/projects)에서만 가능. // 생성/관리는 관리자 전용 페이지(/admin/projects)에서만 가능.
@ -58,7 +58,8 @@ export function ProjectsPage() {
<div className="flex flex-wrap gap-1.5 mt-3"> <div className="flex flex-wrap gap-1.5 mt-3">
<span className="text-[11px] bg-chip-bg text-navy rounded-pill px-2 py-0.5">{p.consultingType}</span> <span className="text-[11px] bg-chip-bg text-navy rounded-pill px-2 py-0.5">{p.consultingType}</span>
<span className="text-[11px] bg-chip-bg text-navy rounded-pill px-2 py-0.5">{p.country}</span> <span className="text-[11px] bg-chip-bg text-navy rounded-pill px-2 py-0.5">{p.country}</span>
<span className="text-[11px] bg-chip-bg text-navy rounded-pill px-2 py-0.5">{SCOPE_LABELS[p.scope] ?? p.scope}</span> {p.scopeText && <span className="text-[11px] bg-chip-bg text-navy rounded-pill px-2 py-0.5"></span>}
{p.scopeGraphic && <span className="text-[11px] bg-chip-bg text-navy rounded-pill px-2 py-0.5"></span>}
</div> </div>
<div className="text-xs text-ink-muted mt-3 flex justify-between"> <div className="text-xs text-ink-muted mt-3 flex justify-between">
<span>PM {p.pmEmail?.split("@")[0] || "—"}</span> <span>PM {p.pmEmail?.split("@")[0] || "—"}</span>
@ -82,7 +83,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
const [productId, setProductId] = useState(""); const [productId, setProductId] = useState("");
const verQ = useQuery({ queryKey: ["versions", productId], queryFn: () => getVersions(productId), enabled: !!productId }); const verQ = useQuery({ queryKey: ["versions", productId], queryFn: () => getVersions(productId), enabled: !!productId });
const [versionId, setVersionId] = useState(""); const [versionId, setVersionId] = useState("");
const [form, setForm] = useState({ name: "", consultingType: "", country: "", scope: "both", pmEmail: "", cautions: "", startDate: "", dueDate: "" }); const [form, setForm] = useState({ name: "", consultingType: "", country: "", scopeText: "", scopeGraphic: "", pmEmail: "", cautions: "", startDate: "", dueDate: "" });
// quick-add master data // quick-add master data
const [newCompany, setNewCompany] = useState(""); const [newCompany, setNewCompany] = useState("");
@ -97,7 +98,6 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
mutationFn: () => createProject({ mutationFn: () => createProject({
...form, companyId, productId, versionId, ...form, companyId, productId, versionId,
companyName: comp?.name, productName: prod?.name, versionName: ver?.label, companyName: comp?.name, productName: prod?.name, versionName: ver?.label,
scope: form.scope as any,
}), }),
onSuccess: () => { qc.invalidateQueries({ queryKey: ["projects"] }); onClose(); }, onSuccess: () => { qc.invalidateQueries({ queryKey: ["projects"] }); onClose(); },
}); });
@ -144,14 +144,13 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
</Field> </Field>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<Field label="컨설팅 종류"><Input value={form.consultingType} onChange={(e) => setForm({ ...form, consultingType: e.target.value })} placeholder="예: 510(k)" /></Field> <Field label="컨설팅 종류"><Input value={form.consultingType} onChange={(e) => setForm({ ...form, consultingType: e.target.value })} placeholder="예: 510(k)" /></Field>
<Field label="제출 국가"><Input value={form.country} onChange={(e) => setForm({ ...form, country: e.target.value })} placeholder="예: 미국(FDA)" /></Field> <Field label="제출 국가"><Input value={form.country} onChange={(e) => setForm({ ...form, country: e.target.value })} placeholder="예: 미국(FDA)" /></Field>
<Field label="계약 범위"> </div>
<Select value={form.scope} onChange={(e) => setForm({ ...form, scope: e.target.value })}> <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
<option value="text"></option><option value="graphic"></option><option value="both">+</option> <Field label="계약 범위 — 글" hint="글 작업 범위를 자유롭게 기술"><Textarea value={form.scopeText} onChange={(e) => setForm({ ...form, scopeText: e.target.value })} placeholder="예: 기술문서·라벨링 작성/검토" /></Field>
</Select> <Field label="계약 범위 — 그림" hint="그림 작업 범위를 자유롭게 기술"><Textarea value={form.scopeGraphic} onChange={(e) => setForm({ ...form, scopeGraphic: e.target.value })} placeholder="예: 도면·UI 목업 제작" /></Field>
</Field>
</div> </div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3"> <div className="grid grid-cols-1 sm:grid-cols-3 gap-3">
<Field label="PM 이메일"><Input value={form.pmEmail} onChange={(e) => setForm({ ...form, pmEmail: e.target.value })} /></Field> <Field label="PM 이메일"><Input value={form.pmEmail} onChange={(e) => setForm({ ...form, pmEmail: e.target.value })} /></Field>

View File

@ -7,7 +7,7 @@ import { CreateProjectModal } from "@/pages/Projects";
import { import {
Card, Button, Badge, PageHeader, EmptyState, LoadingState, Select, Card, Button, Badge, PageHeader, EmptyState, LoadingState, Select,
} from "@/components/ui"; } from "@/components/ui";
import { formatDate, PROJECT_STATUS_META, SCOPE_LABELS } from "@/lib/format"; import { formatDate, PROJECT_STATUS_META } from "@/lib/format";
// 관리자 전용 프로젝트 관리창: 전체 프로젝트 생성·조회·삭제. 세부 관리(작업자·계약· // 관리자 전용 프로젝트 관리창: 전체 프로젝트 생성·조회·삭제. 세부 관리(작업자·계약·
// 분할입금·태스크 등)는 각 프로젝트 상세 페이지의 탭에서 수행. // 분할입금·태스크 등)는 각 프로젝트 상세 페이지의 탭에서 수행.
@ -66,7 +66,7 @@ export function ProjectsAdminPage() {
<td className="text-ink-secondary">{p.companyName} · {p.productName} {p.versionName}</td> <td className="text-ink-secondary">{p.companyName} · {p.productName} {p.versionName}</td>
<td>{p.consultingType}</td> <td>{p.consultingType}</td>
<td>{p.country}</td> <td>{p.country}</td>
<td>{SCOPE_LABELS[p.scope] ?? p.scope}</td> <td>{[p.scopeText && "글", p.scopeGraphic && "그림"].filter(Boolean).join("+") || "—"}</td>
<td className="text-ink-secondary">{p.pmEmail?.split("@")[0] || "—"}</td> <td className="text-ink-secondary">{p.pmEmail?.split("@")[0] || "—"}</td>
<td className="tabular text-ink-muted">{formatDate(p.startDate)} ~ {formatDate(p.dueDate)}</td> <td className="tabular text-ink-muted">{formatDate(p.startDate)} ~ {formatDate(p.dueDate)}</td>
<td>{m && <Badge label={m.label} fg={m.fg} bg={m.bg} dot size="sm" />}</td> <td>{m && <Badge label={m.label} fg={m.fg} bg={m.bg} dot size="sm" />}</td>

View File

@ -170,7 +170,6 @@ export interface Product { id: string; companyId: string; name: string; code: st
export interface Version { id: string; productId: string; label: string; } export interface Version { id: string; productId: string; label: string; }
export type ProjectStatus = "planned" | "active" | "hold" | "done" | "dropped"; export type ProjectStatus = "planned" | "active" | "hold" | "done" | "dropped";
export type Scope = "text" | "graphic" | "both";
export interface Project { export interface Project {
id: string; id: string;
@ -183,7 +182,8 @@ export interface Project {
versionName: string; versionName: string;
consultingType: string; consultingType: string;
country: string; country: string;
scope: Scope; scopeText: string; // 글 계약 범위
scopeGraphic: string; // 그림 계약 범위
pmEmail: string; pmEmail: string;
cautions: string; cautions: string;
status: ProjectStatus; status: ProjectStatus;