diff --git a/src/api/workspace/org.ts b/src/api/workspace/org.ts new file mode 100644 index 00000000..b5cbac2e --- /dev/null +++ b/src/api/workspace/org.ts @@ -0,0 +1,49 @@ +import type { + TApiResult, + TCreateOrgRequest, + TCreateOrgResponse, + TGetOrgResponse, + TMyOrgsData, + TUpdateWorkspaceRequest, + TWorkspace, + TWorkspaceDetail, +} from "@/types/workspace/workspace"; + +import { axiosInstance } from "@/lib/axiosInstance"; + +export const getMyWorkspaces = async (): Promise => { + const { data } = + await axiosInstance.get>("/api/org/my"); + return data.data.organizations; +}; + +export const createWorkspace = async ( + body: TCreateOrgRequest, +): Promise => { + const { data } = await axiosInstance.post>( + "/api/org/create", + body, + ); + return data.data; +}; + +export const getWorkspace = async ( + orgId: number, +): Promise => { + const { data } = await axiosInstance.get>( + `/api/org/${orgId}`, + ); + + return data.data; +}; + +export const updateWorkspace = async ( + orgId: number, + body: TUpdateWorkspaceRequest, +): Promise => { + await axiosInstance.patch>(`/api/org/${orgId}`, body); +}; + +export const deleteWorkspace = async (orgId: number): Promise => { + await axiosInstance.delete>(`/api/org/${orgId}`); +}; diff --git a/src/components/common/dropdownmenu/DropdownMenu.tsx b/src/components/common/dropdownmenu/DropdownMenu.tsx index 14af28a9..5c0f139e 100644 --- a/src/components/common/dropdownmenu/DropdownMenu.tsx +++ b/src/components/common/dropdownmenu/DropdownMenu.tsx @@ -35,10 +35,9 @@ export function DropdownMenu({ }, []); return ( -
+
!v); } }} + className={twMerge(className)} > {trigger}
diff --git a/src/components/common/textarea/TextareaField.tsx b/src/components/common/textarea/TextareaField.tsx index 7e65228f..aabab555 100644 --- a/src/components/common/textarea/TextareaField.tsx +++ b/src/components/common/textarea/TextareaField.tsx @@ -10,6 +10,7 @@ type TProps = { onChange: (e: React.ChangeEvent) => void; className?: string; minRows?: number; + disabled?: boolean; }; export default function TextareaField({ @@ -19,6 +20,7 @@ export default function TextareaField({ placeholder, onChange, className, + disabled = false, minRows = 4, }: TProps) { const ref = useRef(null); @@ -49,6 +51,7 @@ export default function TextareaField({ placeholder={placeholder} onChange={onChange} onInput={resize} + disabled={disabled} />
); diff --git a/src/components/workspace/WorkspaceCard.tsx b/src/components/workspace/WorkspaceCard.tsx index 1f80f7c6..7c14380e 100644 --- a/src/components/workspace/WorkspaceCard.tsx +++ b/src/components/workspace/WorkspaceCard.tsx @@ -18,7 +18,6 @@ export default function WorkspaceCard({ workspace: w, menuItems }: TProps) {
  • - {/* TODO: 스타일 확인을 위해 bg-gray-100넣어둠. API연동할때 삭제예정 */} {w.logoUrl ? ( { + if (axios.isAxiosError(e)) { + const message = (e.response?.data as { message?: unknown } | undefined) + ?.message; + if (typeof message === "string" && message.trim().length > 0) { + return message; + } + } + return fallback; +}; diff --git a/src/pages/workspace/Workspace.tsx b/src/pages/workspace/Workspace.tsx index f32f74ff..c55ca643 100644 --- a/src/pages/workspace/Workspace.tsx +++ b/src/pages/workspace/Workspace.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from "react"; import { useNavigate } from "react-router-dom"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { TWorkspace } from "@/types/workspace/workspace"; @@ -10,11 +11,13 @@ 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"; import UpLoadImgIcon from "@/assets/icon/workspace/uploadImg.svg?react"; import UserProfileIcon from "@/assets/icon/workspace/userProfile.svg?react"; +import { getAxiosMessage } from "@/lib/getAxiosMessage"; export default function WorkspacePage() { const navigate = useNavigate(); @@ -24,44 +27,54 @@ export default function WorkspacePage() { const [newName, setNewName] = useState(""); const [newDesc, setNewDesc] = useState(""); + + const queryClient = useQueryClient(); + const workspacesQuery = useQuery({ + queryKey: ["my-workspaces"], + queryFn: getMyWorkspaces, + }); + + const createWorkspaceMutation = useMutation({ + mutationFn: createWorkspace, + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ["my-workspaces"] }); + setCreateOpen(false); + }, + }); + const listLoading = workspacesQuery.isLoading || workspacesQuery.isFetching; + const listErrorMsg = workspacesQuery.isError + ? getAxiosMessage( + workspacesQuery.error, + "워크스페이스 목록 조회중 오류가 발생했습니다", + ) + : null; + + const creating = createWorkspaceMutation.isPending; + const createErrorMsg = createWorkspaceMutation.isError + ? getAxiosMessage( + createWorkspaceMutation.error, + "워크스페이스 생성 중 오류가 발생했습니다", + ) + : null; + const fileRef = useRef(null); const openFile = () => { if (!fileRef.current) return; fileRef.current.value = ""; fileRef.current.click(); }; - 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 workspaces = workspacesQuery.data ?? []; + const filtered = useMemo(() => { const q = query.trim().toLowerCase(); if (!q) return workspaces; return workspaces.filter((w) => w.name.toLowerCase().includes(q)); }, [query, workspaces]); - const menuItems = (id: TWorkspace["id"]): TMenuItem[] => [ + const menuItems = (id: TWorkspace["orgId"]): TMenuItem[] => [ { icon: , label: "정보 수정하기", @@ -75,39 +88,38 @@ export default function WorkspacePage() { onClick: () => alert("멤버 관리 기능은 추후 연결 예정"), }, ]; + + const onCloseCreate = () => { + if (logoPreview) URL.revokeObjectURL(logoPreview); + setLogoPreview(null); + setCreateOpen(false); + }; const onOpenCreate = () => { setNewName(""); setNewDesc(""); + if (logoPreview) URL.revokeObjectURL(logoPreview); + setLogoPreview(null); + createWorkspaceMutation.reset(); setCreateOpen(true); }; const onPickFile = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; - setLogoFile(file); setLogoPreview(URL.createObjectURL(file)); }; - // TODO: 모달 닫힐때 preview URL 해제필요 useEffect(() => { return () => { if (logoPreview) URL.revokeObjectURL(logoPreview); }; }, [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 연동 후에 생성 기능 연결예정"); + const name = newName.trim(); + const description = newDesc.trim(); + if (!name) return; + createWorkspaceMutation.mutate({ name, description, logoUrl: null }); }; return ( @@ -139,26 +151,44 @@ export default function WorkspacePage() { 워크스페이스 생성하기
    -
      - {filtered.map((w) => ( - - ))} - {filtered.length === 0 && ( -
    • -

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

      -
    • - )} -
    - setCreateOpen(false)} - size="xl" - padding="lg" - > + + {listLoading && ( +
    +

    불러오는중..

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

    {listErrorMsg}

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

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

      +
    • + )} +
    + )} + +

    워크스페이스 생성 @@ -204,6 +234,7 @@ export default function WorkspacePage() { placeholder="조직의 이름을 입력하세요." value={newName} onChange={(e) => setNewName(e.target.value)} + disabled={creating} /> setNewDesc(e.target.value)} + disabled={creating} /> + {createErrorMsg && ( +

    {createErrorMsg}

    + )}

    diff --git a/src/pages/workspace/WorkspaceSetting.tsx b/src/pages/workspace/WorkspaceSetting.tsx index 9eca1860..22e89818 100644 --- a/src/pages/workspace/WorkspaceSetting.tsx +++ b/src/pages/workspace/WorkspaceSetting.tsx @@ -1,27 +1,113 @@ -import { useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; +import { toast } from "sonner"; import Button from "@/components/common/button/Button"; import Input from "@/components/common/input/Input"; import Modal from "@/components/common/modal/Modal"; +import TextareaField from "@/components/common/textarea/TextareaField"; +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"; +import { getAxiosMessage } from "@/lib/getAxiosMessage"; export default function WorkspaceSetting() { const navigate = useNavigate(); const { workspaceId } = useParams(); + + const orgId = useMemo(() => { + if (workspaceId === null) return null; + const n = Number(workspaceId); + return Number.isFinite(n) && n > 0 ? n : null; + }, [workspaceId]); const [name, setName] = useState(""); const [desc, setDesc] = useState(""); + const [loading, setLoading] = useState(false); + + const [saving, setSaving] = useState(false); + const [errorMsg, setErrorMsg] = useState(null); const [deleteOpen, setDeleteOpen] = useState(false); - const onSave = () => { - alert(`API 연동후 저장예정 (workspaceId=${workspaceId})`); + const [deleting, setDeleting] = useState(false); + + const [logoUrl, setLogoUrl] = 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 ?? ""); + setLogoUrl(detail.logoUrl ?? null); + } catch (e) { + const message = getAxiosMessage( + e, + "워크스페이스 정보를 불러오지 못했습니다", + ); + setErrorMsg(message); + toast.error(message); + } finally { + setLoading(false); + } + }; + useEffect(() => { + void fetchWorkspaceDetail(); + }, [orgId]); + + const onSave = async () => { + if (orgId === null) { + setErrorMsg("잘못된 워크스페이스ID 입니다"); + return; + } + const nextName = name.trim(); + const nextDesc = desc.trim(); + if (!nextName) return; + setSaving(true); + + try { + await updateWorkspace(orgId, { + name: nextName, + description: nextDesc, + logoUrl, + }); + toast.success("변경사항이 저장되었습니다"); + await fetchWorkspaceDetail(); + } catch (e) { + toast.error(getAxiosMessage(e, "변경사항 저장에 실패했습니다")); + } finally { + setSaving(false); + } }; - const onDelete = () => { - alert(`API 연동후 삭제예정 (workspaceId=${workspaceId})`); - setDeleteOpen(false); - navigate("/workspace", { replace: true }); + const onDelete = async () => { + if (orgId === null) { + setErrorMsg("잘못된 워크스페이스ID 입니다"); + return; + } + setDeleting(true); + try { + await deleteWorkspace(orgId); + toast.success("워크스페이스가 삭제되었습니다"); + setDeleteOpen(false); + navigate("/workspace", { replace: true }); + } catch (e) { + toast.error(getAxiosMessage(e, "워크스페이스 삭제에 실패했습니다")); + } finally { + setDeleting(false); + } + }; + + const openDeleteModal = () => { + setDeleteOpen(true); }; return ( @@ -32,126 +118,157 @@ export default function WorkspaceSetting() { 워크스페이스 정보를 확인하고 관리하세요.

    -
    -

    기본 정보

    -

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

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