feat(project): 계약범위 글/그림 입력칸 + 인턴/직책/초과근무/로고/로그아웃 개선분
All checks were successful
build-and-push / build (push) Successful in 30s
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:
parent
e3b5a874b3
commit
581fd7a19f
@ -119,6 +119,7 @@ export const STAGE_KIND_LABELS: Record<string, string> = {
|
||||
final: "잔금",
|
||||
};
|
||||
|
||||
// 인센티브 BE/non-BE 라벨 (프로젝트 계약범위와 무관 — 그쪽은 자유 텍스트)
|
||||
export const SCOPE_LABELS: Record<string, string> = {
|
||||
be: "BE",
|
||||
non_be: "non-BE",
|
||||
|
||||
@ -18,7 +18,7 @@ import {
|
||||
import { Gantt } from "@/components/Gantt";
|
||||
import { Kanban } from "@/components/Kanban";
|
||||
import {
|
||||
formatDate, formatWon, formatSize, PROJECT_STATUS_META, SCOPE_LABELS, LANE_LABELS, classNames,
|
||||
formatDate, formatWon, formatSize, PROJECT_STATUS_META, LANE_LABELS, classNames,
|
||||
} from "@/lib/format";
|
||||
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>
|
||||
<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>}
|
||||
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>
|
||||
<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>
|
||||
<Row label="컨설팅 종류" value={p.consultingType} />
|
||||
<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} />
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
Card, Button, Badge, PageHeader, Modal, Field, Input, Select, Textarea,
|
||||
EmptyState, LoadingState,
|
||||
} from "@/components/ui";
|
||||
import { formatDate, PROJECT_STATUS_META, SCOPE_LABELS } from "@/lib/format";
|
||||
import { formatDate, PROJECT_STATUS_META } from "@/lib/format";
|
||||
|
||||
// 나의 업무 > 프로젝트: 본인이 참여한 프로젝트만 보는 read-only 뷰 (관리자·유저 동일).
|
||||
// 생성/관리는 관리자 전용 페이지(/admin/projects)에서만 가능.
|
||||
@ -58,7 +58,8 @@ export function ProjectsPage() {
|
||||
<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.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 className="text-xs text-ink-muted mt-3 flex justify-between">
|
||||
<span>PM {p.pmEmail?.split("@")[0] || "—"}</span>
|
||||
@ -82,7 +83,7 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
const [productId, setProductId] = useState("");
|
||||
const verQ = useQuery({ queryKey: ["versions", productId], queryFn: () => getVersions(productId), enabled: !!productId });
|
||||
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
|
||||
const [newCompany, setNewCompany] = useState("");
|
||||
@ -97,7 +98,6 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
mutationFn: () => createProject({
|
||||
...form, companyId, productId, versionId,
|
||||
companyName: comp?.name, productName: prod?.name, versionName: ver?.label,
|
||||
scope: form.scope as any,
|
||||
}),
|
||||
onSuccess: () => { qc.invalidateQueries({ queryKey: ["projects"] }); onClose(); },
|
||||
});
|
||||
@ -144,14 +144,13 @@ export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
||||
</Field>
|
||||
</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.country} onChange={(e) => setForm({ ...form, country: e.target.value })} placeholder="예: 미국(FDA)" /></Field>
|
||||
<Field label="계약 범위">
|
||||
<Select value={form.scope} onChange={(e) => setForm({ ...form, scope: e.target.value })}>
|
||||
<option value="text">글</option><option value="graphic">그림</option><option value="both">글+그림</option>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||
<Field label="계약 범위 — 글" hint="글 작업 범위를 자유롭게 기술"><Textarea value={form.scopeText} onChange={(e) => setForm({ ...form, scopeText: e.target.value })} placeholder="예: 기술문서·라벨링 작성/검토" /></Field>
|
||||
<Field label="계약 범위 — 그림" hint="그림 작업 범위를 자유롭게 기술"><Textarea value={form.scopeGraphic} onChange={(e) => setForm({ ...form, scopeGraphic: e.target.value })} placeholder="예: 도면·UI 목업 제작" /></Field>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
@ -7,7 +7,7 @@ import { CreateProjectModal } from "@/pages/Projects";
|
||||
import {
|
||||
Card, Button, Badge, PageHeader, EmptyState, LoadingState, Select,
|
||||
} 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>{p.consultingType}</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="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>
|
||||
|
||||
@ -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 type ProjectStatus = "planned" | "active" | "hold" | "done" | "dropped";
|
||||
export type Scope = "text" | "graphic" | "both";
|
||||
|
||||
export interface Project {
|
||||
id: string;
|
||||
@ -183,7 +182,8 @@ export interface Project {
|
||||
versionName: string;
|
||||
consultingType: string;
|
||||
country: string;
|
||||
scope: Scope;
|
||||
scopeText: string; // 글 계약 범위
|
||||
scopeGraphic: string; // 그림 계약 범위
|
||||
pmEmail: string;
|
||||
cautions: string;
|
||||
status: ProjectStatus;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user