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
24 changes: 21 additions & 3 deletions src/shared/ui/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import Link from "next/link";
import {
forwardRef,
useState,
type ButtonHTMLAttributes,
type InputHTMLAttributes,
type ReactNode,
Expand Down Expand Up @@ -270,6 +271,19 @@ export function EmptyState({ title, desc, action }: { title: string; desc?: stri
}

export function CopyField({ value, label }: { value: string; label?: string }) {
const [copied, setCopied] = useState<"ok" | "fail" | null>(null);
const copy = () => {
const flash = (r: "ok" | "fail") => {
setCopied(r);
setTimeout(() => setCopied(null), 1500);
};
// clipboard는 https·localhost에서만 있다
if (!navigator.clipboard) return flash("fail");
navigator.clipboard.writeText(value).then(
() => flash("ok"),
() => flash("fail"),
);
};
return (
<div className="flex items-center gap-2 rounded-xl border border-ink-200 bg-ink-50 px-3 py-2">
<div className="min-w-0 flex-1">
Expand All @@ -278,10 +292,14 @@ export function CopyField({ value, label }: { value: string; label?: string }) {
</div>
<button
type="button"
onClick={() => navigator.clipboard?.writeText(value).catch(() => undefined)}
className="shrink-0 rounded-lg border border-ink-200 bg-white px-2.5 py-1 text-xs font-medium text-ink-700 hover:bg-ink-100"
onClick={copy}
aria-live="polite"
className={cn(
"shrink-0 whitespace-nowrap rounded-lg border border-ink-200 bg-white px-2.5 py-1 text-xs font-medium hover:bg-ink-100",
copied === "ok" ? "text-auto" : copied === "fail" ? "text-forbidden" : "text-ink-700",
)}
>
복사
{copied === "ok" ? "복사됨 ✓" : copied === "fail" ? "복사 실패" : "복사"}
</button>
</div>
);
Expand Down
4 changes: 2 additions & 2 deletions src/widgets/activity/ActivityView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ export function ActivityView({ projectId }: { projectId: string }) {
상세에서 확인합니다.
</p>
</div>
<div className="inline-flex rounded-lg bg-ink-100 p-0.5">
<div className="inline-flex shrink-0 self-start rounded-lg bg-ink-100 p-0.5 sm:self-auto">
{(Object.keys(TABS) as Tab[]).map((t) => (
<button
key={t}
onClick={() => setTab(t)}
className={cn(
"h-8 rounded-md px-3 text-[13px] font-medium",
"h-7 whitespace-nowrap rounded-md px-2.5 text-[12px] font-medium",
tab === t ? "bg-white shadow-card" : "text-ink-600",
)}
>
Expand Down
18 changes: 12 additions & 6 deletions src/widgets/project-list/ProjectListView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,18 @@ export function ProjectListView() {
<p className="text-[14px] font-medium">
아직 배정된 프로젝트가 없습니다. 대표가 배정하면 여기에 나타납니다
</p>
<p className="mt-1 text-[13px] text-ink-500">
배정되려면 에이전트가 연결돼 있어야 합니다.{" "}
<Link href="/connect" className="font-medium text-ink-900 hover:underline">
에이전트 연결
</Link>
</p>
{myAgent ? (
<p className="mt-1 text-[13px] text-ink-500">
에이전트 연결됨 · {myAgent.agentName}. 대표의 배정을 기다리는 중입니다.
</p>
) : (
<p className="mt-1 text-[13px] text-ink-500">
배정되려면 에이전트가 연결돼 있어야 합니다.{" "}
<Link href="/connect" className="font-medium text-ink-900 hover:underline">
에이전트 연결
</Link>
</p>
)}
</>
)}
</li>
Expand Down
48 changes: 38 additions & 10 deletions src/widgets/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
useStartProject,
useUnassignMember,
} from "@/entities/project";
import { TEAM_ROLE_META, TeamRoleBadge } from "@/entities/task";
import { TEAM_ROLE_META, TeamRoleBadge, useTasks } from "@/entities/task";
import { useCurrentUser } from "@/entities/user";

const ROLES: TeamRole[] = ["FRONTEND", "BACKEND"];
Expand Down Expand Up @@ -279,6 +279,12 @@ function StartSection({
const start = useStartProject(projectId);
const [confirming, setConfirming] = useState(false);
const details = errorDetails(start.error);
// 미리 막아 두는 안내일 뿐 판정은 서버가 한다 (시작 시 400 details가 정본)
const tasks = useTasks(projectId).data;
const unassigned = ROLES.filter(
(role) => tasks?.some((t) => t.teamRole === role) && !members.some((m) => m.teamRole === role),
);
const noTasks = tasks?.length === 0;

return (
<section id="start" className="card scroll-mt-6 p-5">
Expand Down Expand Up @@ -324,15 +330,37 @@ function StartSection({
</div>
</div>
) : (
<Button
className="mt-3"
onClick={() => {
start.reset();
setConfirming(true);
}}
>
<Play size={14} /> 프로젝트 시작
</Button>
<>
{(unassigned.length > 0 || noTasks) && (
<p className="mt-3 text-[13px] text-human">
{noTasks ? (
<>
태스크가 없습니다.{" "}
<Link href={`/p/${projectId}/rooms/owner`} className="font-medium underline underline-offset-2">
Room 3에서 PM 계획 받기
</Link>
</>
) : (
<>
{unassigned.map((r) => TEAM_ROLE_META[r].label).join(", ")} 역할에 배정된 에이전트가 없습니다.{" "}
<a href="#members" className="font-medium underline underline-offset-2">
역할 배정으로
</a>
</>
)}
</p>
)}
<Button
className="mt-3"
disabled={unassigned.length > 0 || noTasks}
onClick={() => {
start.reset();
setConfirming(true);
}}
>
<Play size={14} /> 프로젝트 시작
</Button>
</>
)}
</>
)}
Expand Down
Loading