From 4bb2155cfb0c2b7fdaa86ef867e7ff229762129f Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Sun, 1 Mar 2026 16:32:40 +0900 Subject: [PATCH 01/14] =?UTF-8?q?feat:=20=EC=9B=8C=ED=81=AC=EC=8A=A4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=A1=B0=ED=9A=8C/=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20api=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/workspace/org.ts | 32 +++++++ src/pages/workspace/Workspace.tsx | 148 +++++++++++++++++++----------- src/types/workspace/mapper.ts | 9 ++ src/types/workspace/workspace.ts | 29 ++++++ 4 files changed, 166 insertions(+), 52 deletions(-) create mode 100644 src/api/workspace/org.ts create mode 100644 src/types/workspace/mapper.ts diff --git a/src/api/workspace/org.ts b/src/api/workspace/org.ts new file mode 100644 index 00000000..3f2049ac --- /dev/null +++ b/src/api/workspace/org.ts @@ -0,0 +1,32 @@ +import { toWorkspace } from "@/types/workspace/mapper"; +import type { + TApiResult, + TCreateOrgRequest, + TCreateOrgResponse, + TMyOrgsData, + TWorkspace, +} from "@/types/workspace/workspace"; + +import { authInstance } from "@/lib/axiosInstance"; + +export const getMyWorkspaces = async (): Promise => { + const { data } = + await authInstance.get>("/api/org/my"); + if (data.status !== "200") { + throw new Error("워크스페이스 조회에 실패했습니다"); + } + return data.data.organizations.map(toWorkspace); +}; + +export const createWorkspace = async ( + body: TCreateOrgRequest, +): Promise => { + const { data } = await authInstance.post>( + "/api/org/create", + body, + ); + if (data.status !== "200") { + throw new Error("워크스페이스 생성에 실패했습니다"); + } + return data.data; +}; diff --git a/src/pages/workspace/Workspace.tsx b/src/pages/workspace/Workspace.tsx index f32f74ff..7ac927f1 100644 --- a/src/pages/workspace/Workspace.tsx +++ b/src/pages/workspace/Workspace.tsx @@ -10,6 +10,7 @@ import Modal from "@/components/common/modal/Modal"; import TextareaField from "@/components/common/textarea/TextareaField"; import WorkspaceCard from "@/components/workspace/WorkspaceCard"; +import { createWorkspace, getMyWorkspaces } from "@/api/workspace/org"; import EditContainIcon from "@/assets/icon/workspace/edit-contained.svg?react"; import PlusIcon from "@/assets/icon/workspace/plus.svg?react"; import SearchIcon from "@/assets/icon/workspace/search.svg?react"; @@ -24,6 +25,12 @@ export default function WorkspacePage() { const [newName, setNewName] = useState(""); const [newDesc, setNewDesc] = useState(""); + + const [workspaces, setWorkspaces] = useState([]); + const [loading, setLoading] = useState(false); + const [listErrorMsg, setListErrorMsg] = useState(null); + const [creating, setCreating] = useState(false); + const [createErrorMsg, setCreateErrorMsg] = useState(null); const fileRef = useRef(null); const openFile = () => { if (!fileRef.current) return; @@ -32,29 +39,7 @@ export default function WorkspacePage() { }; const [logoFile, setLogoFile] = useState(null); const [logoPreview, setLogoPreview] = useState(null); - const workspaces: TWorkspace[] = useMemo( - () => [ - { - id: "1", - name: "광고회사1", - description: "광고회사1입니다.", - myRole: "admin", - }, - { - id: "2", - name: "광고회사2", - description: "광고회사2입니다.", - myRole: "admin", - }, - { - id: "3", - name: "광고회사3", - description: "광고회사3입니다.", - myRole: "admin", - }, - ], - [], - ); + const filtered = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return workspaces; @@ -78,6 +63,10 @@ export default function WorkspacePage() { const onOpenCreate = () => { setNewName(""); setNewDesc(""); + setLogoFile(null); + setLogoPreview(null); + setCreateErrorMsg(null); + setListErrorMsg(null); setCreateOpen(true); }; @@ -95,19 +84,52 @@ export default function WorkspacePage() { }; }, [logoPreview]); - // TODO: API 연동 후에 생성 동작 연결하기 - const onSubmitCreate = () => { - const form = new FormData(); - form.append("name", newName.trim()); - form.append("description", newDesc.trim()); - if (logoFile) form.append("logo", logoFile); - - console.log("create payload", { - name: newName.trim(), - description: newDesc.trim(), - logoFile, - }); - alert("API 연동 후에 생성 기능 연결예정"); + // 워크스페이스 목록 조회 함수 (길어지면, util함수로 빼기) + const fetchWorkspaces = async () => { + setLoading(true); + setListErrorMsg(null); + try { + const list = await getMyWorkspaces(); + setWorkspaces(list); + } catch (e) { + const message = + e instanceof Error + ? e.message + : "워크스페이스 목록 조회중 오류가 발생했습니다"; + setListErrorMsg(message); + setWorkspaces([]); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + void fetchWorkspaces(); + }, []); + + const onSubmitCreate = async () => { + const name = newName.trim(); + const description = newDesc.trim(); + if (!name) return; + setCreating(true); + setCreateErrorMsg(null); + try { + await createWorkspace({ + name, + description, + logoUrl: null, + }); + setCreateOpen(false); + await fetchWorkspaces(); + } catch (e) { + let message = "워크스페이스 생성 중 오류가 발생했습니다"; + if (e instanceof Error) { + message = e.message; + } + setCreateErrorMsg(message); + } finally { + setCreating(false); + } }; return ( @@ -139,20 +161,39 @@ export default function WorkspacePage() { 워크스페이스 생성하기 -
    - {filtered.map((w) => ( - - ))} - {filtered.length === 0 && ( -
  • -

    워크스페이스가 없습니다.

    -
  • - )} -
+ + {loading && ( +
+

불러오는중..

+
+ )} + {!loading && listErrorMsg && ( +
+

{listErrorMsg}

+ +
+ )} + {!loading && !listErrorMsg && ( +
    + {filtered.map((w) => ( + + ))} + {filtered.length === 0 && ( +
  • +

    + 워크스페이스가 없습니다. +

    +
  • + )} +
+ )} + setCreateOpen(false)} @@ -212,15 +253,18 @@ export default function WorkspacePage() { value={newDesc} onChange={(e) => setNewDesc(e.target.value)} /> + {createErrorMsg && ( +

{createErrorMsg}

+ )} diff --git a/src/types/workspace/mapper.ts b/src/types/workspace/mapper.ts new file mode 100644 index 00000000..c2084bcb --- /dev/null +++ b/src/types/workspace/mapper.ts @@ -0,0 +1,9 @@ +import type { TWorkspace, TWorkspaceDTO } from "./workspace"; + +export const toWorkspace = (dto: TWorkspaceDTO): TWorkspace => ({ + id: String(dto.orgId), + name: dto.name, + description: dto.description, + logoUrl: dto.logoUrl, + myRole: dto.myRole, +}); diff --git a/src/types/workspace/workspace.ts b/src/types/workspace/workspace.ts index f66b16bb..99571212 100644 --- a/src/types/workspace/workspace.ts +++ b/src/types/workspace/workspace.ts @@ -7,3 +7,32 @@ export type TWorkspace = { logoUrl?: string | null; myRole: string; }; + +// UI구현할때, id로 해놓았는데, DTO코드작성하지 말고, swagger에 맞게 코드 작성해볼까? +export type TWorkspaceDTO = { + orgId: number; + name: string; + description: string; + logoUrl: string | null; + myRole: string; +}; +export type TApiResult = { + status: string; + data: T; +}; + +export type TMyOrgsData = { + organizations: TWorkspaceDTO[]; +}; + +export type TCreateOrgRequest = { + name: string; + description: string; + logoUrl?: string | null; +}; + +export type TCreateOrgResponse = { + name: string; + description: string; + logoUrl: string | null; +}; From 80721444f21422c6720c8d27b15b004aba32e57e Mon Sep 17 00:00:00 2001 From: JAESEON PARK Date: Sun, 1 Mar 2026 23:46:17 +0900 Subject: [PATCH 02/14] =?UTF-8?q?feat:=20=EC=9B=8C=ED=81=AC=EC=8A=A4?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EA=B0=9C=EB=B3=84=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C/=EC=88=98=EC=A0=95/=EC=82=AD=EC=A0=9C=20API=EC=97=B0?= =?UTF-8?q?=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/workspace/org.ts | 50 +++- src/pages/workspace/Workspace.tsx | 2 +- src/pages/workspace/WorkspaceSetting.tsx | 363 +++++++++++++++-------- src/types/workspace/mapper.ts | 15 +- src/types/workspace/workspace.ts | 22 ++ 5 files changed, 331 insertions(+), 121 deletions(-) diff --git a/src/api/workspace/org.ts b/src/api/workspace/org.ts index 3f2049ac..30847d20 100644 --- a/src/api/workspace/org.ts +++ b/src/api/workspace/org.ts @@ -1,10 +1,13 @@ -import { toWorkspace } from "@/types/workspace/mapper"; +import { toWorkspace, toWorkspaceDetail } from "@/types/workspace/mapper"; import type { TApiResult, TCreateOrgRequest, TCreateOrgResponse, + TGetOrgResponse, TMyOrgsData, + TUpdateWorkspaceRequest, TWorkspace, + TWorkspaceDetail, } from "@/types/workspace/workspace"; import { authInstance } from "@/lib/axiosInstance"; @@ -30,3 +33,48 @@ export const createWorkspace = async ( } return data.data; }; + +export const getWorkspace = async ( + orgId: number, +): Promise => { + const { data } = await authInstance.get>( + `/api/org/${orgId}`, + ); + if (data.status !== "200") { + if (data.status === "404_1") + throw new Error("해당 조직은 존재하지 않습니다"); + if (data.status === "410_1") throw new Error("해당 조직은 삭제되었습니다"); + throw new Error("워크스페이스 정보를 불러오지 못했습니다"); + } + return toWorkspaceDetail(data.data); +}; + +export const updateWorkspace = async ( + orgId: number, + body: TUpdateWorkspaceRequest, +): Promise => { + const { data } = await authInstance.patch>( + `/api/org/${orgId}`, + body, + ); + if (data.status !== "200") { + if (data.status === "404_1") + throw new Error("해당 조직은 존재하지 않습니다"); + if (data.status === "403_1") + throw new Error("해당 조직에 대한 권한이 없습니다."); + throw new Error("워크스페이스 수정에 실패했습니다"); + } +}; + +export const deleteWorkspace = async (orgId: number): Promise => { + const { data } = await authInstance.delete>( + `/api/org/${orgId}`, + ); + if (data.status !== "200") { + if (data.status === "404_1") + throw new Error("해당 조직은 존재하지 않습니다"); + if (data.status === "403_1") + throw new Error("해당 조직에 대한 권한이 없습니다."); + throw new Error("워크스페이스 삭제에 실패했습니다"); + } +}; diff --git a/src/pages/workspace/Workspace.tsx b/src/pages/workspace/Workspace.tsx index 7ac927f1..beee9d11 100644 --- a/src/pages/workspace/Workspace.tsx +++ b/src/pages/workspace/Workspace.tsx @@ -31,6 +31,7 @@ export default function WorkspacePage() { const [listErrorMsg, setListErrorMsg] = useState(null); const [creating, setCreating] = useState(false); const [createErrorMsg, setCreateErrorMsg] = useState(null); + const fileRef = useRef(null); const openFile = () => { if (!fileRef.current) return; @@ -66,7 +67,6 @@ export default function WorkspacePage() { setLogoFile(null); setLogoPreview(null); setCreateErrorMsg(null); - setListErrorMsg(null); setCreateOpen(true); }; diff --git a/src/pages/workspace/WorkspaceSetting.tsx b/src/pages/workspace/WorkspaceSetting.tsx index 9eca1860..58910d8d 100644 --- a/src/pages/workspace/WorkspaceSetting.tsx +++ b/src/pages/workspace/WorkspaceSetting.tsx @@ -1,10 +1,15 @@ -import { useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import Button from "@/components/common/button/Button"; import Input from "@/components/common/input/Input"; import Modal from "@/components/common/modal/Modal"; +import { + deleteWorkspace, + getWorkspace, + updateWorkspace, +} from "@/api/workspace/org"; import MessageCircleWarningIcon from "@/assets/icon/workspace/message-circle-warning.svg?react"; import UpLoadImgIcon from "@/assets/icon/workspace/uploadImg.svg?react"; import WarningIcon from "@/assets/icon/workspace/warning.svg?react"; @@ -12,16 +17,101 @@ import WarningIcon from "@/assets/icon/workspace/warning.svg?react"; export default function WorkspaceSetting() { const navigate = useNavigate(); const { workspaceId } = useParams(); + + const orgId = useMemo(() => { + const n = Number(workspaceId); + return Number.isFinite(n) ? n : null; + }, [workspaceId]); const [name, setName] = useState(""); const [desc, setDesc] = useState(""); + const [loading, setLoading] = useState(false); + const [errorMsg, setErrorMsg] = useState(null); + + const [saving, setSaving] = useState(false); + const [saveErrorMsg, setSaveErrorMsg] = useState(null); const [deleteOpen, setDeleteOpen] = useState(false); - const onSave = () => { - alert(`API 연동후 저장예정 (workspaceId=${workspaceId})`); + const [deleting, setDeleting] = useState(false); + const [deleteErrorMsg, setDeleteErrorMsg] = useState(null); + + const fetchWorkspaceDetail = async () => { + if (orgId === null) { + setErrorMsg("잘못된 워크스페이스ID 입니다"); + return; + } + setLoading(true); + setErrorMsg(null); + try { + const detail = await getWorkspace(orgId); + setName(detail.name); + setDesc(detail.description ?? ""); + } catch (e) { + const message = + e instanceof Error + ? e.message + : "워크스페이스 정보를 불러오지 못했습니다."; + setErrorMsg(message); + } finally { + setLoading(false); + } + }; + useEffect(() => { + void fetchWorkspaceDetail(); + }, [orgId]); + + const onSave = async () => { + if (orgId === null) { + setSaveErrorMsg("잘못된 워크스페이스ID 입니다"); + return; + } + const nextName = name.trim(); + const nextDesc = desc.trim(); + if (!nextName) return; + setSaving(true); + setSaveErrorMsg(null); + + try { + await updateWorkspace(orgId, { + name: nextName, + description: nextDesc, + logoUrl: null, + }); + await fetchWorkspaceDetail(); + setSaveErrorMsg(null); + } catch (e) { + const message = + e instanceof Error + ? e.message + : "워크스페이스 수정 중 오류가 발생했습니다"; + setSaveErrorMsg(message); + } finally { + setSaving(false); + } + }; + const onDelete = async () => { + if (orgId === null) { + setDeleteErrorMsg("잘못된 워크스페이스ID 입니다"); + return; + } + setDeleting(true); + setDeleteErrorMsg(null); + try { + await deleteWorkspace(orgId); + setDeleteOpen(false); + navigate("/workspace", { replace: true }); + } catch (e) { + const message = + e instanceof Error + ? e.message + : "워크스페이스 삭제 중 오류가 발생했습니다"; + setDeleteErrorMsg(message); + } finally { + setDeleting(false); + } }; - const onDelete = () => { - alert(`API 연동후 삭제예정 (workspaceId=${workspaceId})`); - setDeleteOpen(false); - navigate("/workspace", { replace: true }); + + const openDeleteModal = () => { + setDeleteErrorMsg(null); + setDeleteOpen(true); }; return ( @@ -32,126 +122,163 @@ export default function WorkspaceSetting() { 워크스페이스 정보를 확인하고 관리하세요.

-
-

기본 정보

-

- 워크스페이스의 대표적인 정보를 설정합니다. -

-
-
-
로고 이미지
-
- -
-
- - -
-
-
- setName(e.target.value)} - /> -
- -