From 20744d9e1430793da677128b63c2420a85f847a6 Mon Sep 17 00:00:00 2001 From: JeongHyeon <0429cjhn@naver.com> Date: Mon, 7 Jul 2025 22:29:20 +0900 Subject: [PATCH 01/13] =?UTF-8?q?feat:=20=EC=82=AC=EC=9E=A5=EB=8B=98=20?= =?UTF-8?q?=EC=95=8C=EB=B0=94=EA=B7=BC=EB=AC=B4=EC=A7=80=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EC=82=AC=EC=97=85=EC=9E=90=EB=B2=88=ED=98=B8=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.production | 2 +- src/api/albaAdd.ts | 21 +++ src/components/Modal/AlbaAddModal.tsx | 229 +++----------------------- 3 files changed, 41 insertions(+), 211 deletions(-) create mode 100644 src/api/albaAdd.ts diff --git a/.env.production b/.env.production index bb04dc5..9a90a3c 100644 --- a/.env.production +++ b/.env.production @@ -1,2 +1,2 @@ # .env.production -VITE_API_BASE_URL=http://3.39.237.218:8080 \ No newline at end of file +VITE_API_BASE_URL=http://43.200.176.79:8080 \ No newline at end of file diff --git a/src/api/albaAdd.ts b/src/api/albaAdd.ts new file mode 100644 index 0000000..c494c6f --- /dev/null +++ b/src/api/albaAdd.ts @@ -0,0 +1,21 @@ +import axiosInstance from "./axios"; + +interface AddStoreRequest { + name: string; + location: string; + businessNumber: string; + storeCode: string; + requireApproval: boolean; + defaultHourlyWage: number; +} + +export const validateBusinessNumber = (businessNumber: string) => { + console.log("✅ 현재 baseURL 확인:", import.meta.env.VITE_API_URL); + + return axiosInstance.post("/store/validate-business-number", { businessNumber }); +}; + + +export const registerStore = (data: AddStoreRequest) => { + return axiosInstance.post("/store", data); +}; diff --git a/src/components/Modal/AlbaAddModal.tsx b/src/components/Modal/AlbaAddModal.tsx index 7d1152d..2f8891f 100644 --- a/src/components/Modal/AlbaAddModal.tsx +++ b/src/components/Modal/AlbaAddModal.tsx @@ -1,7 +1,7 @@ import styles from "./AlbaAddModal.module.css"; import { useState } from "react"; import Button from "../Button"; -import axiosInstance from "../../api/axios"; +import { validateBusinessNumber } from "../../api/albaAdd"; interface AlbaAddModalProps { onClose: () => void; @@ -9,169 +9,45 @@ interface AlbaAddModalProps { const AlbaAddModal: React.FC = ({ onClose }) => { const [businessNumber, setBusinessNumber] = useState(""); - const [shop, setShop] = useState(""); const [isValidating, setIsValidating] = useState(false); const [validationError, setValidationError] = useState(""); const handleBusinessNumber = (e: React.ChangeEvent) => { - // 하이픈 자동 포맷팅 (000-00-00000) - let value = e.target.value; - // 숫자와 하이픈만 입력 가능하도록 - value = value.replace(/[^\d-]/g, ''); - - // 하이픈 자동 추가 - if (value.length > 0) { - // 기존 하이픈 제거 - value = value.replace(/-/g, ''); - - // 숫자만 최대 10자리 제한 - if (value.length > 10) { - value = value.substring(0, 10); - } - - // 하이픈 추가 (XXX-XX-XXXXX 형식) - if (value.length > 5) { - value = `${value.substring(0, 3)}-${value.substring(3, 5)}-${value.substring(5)}`; - } else if (value.length > 3) { - value = `${value.substring(0, 3)}-${value.substring(3)}`; - } - } - + let value = e.target.value.replace(/[^\d]/g, ""); + if (value.length > 10) value = value.slice(0, 10); + if (value.length > 5) value = `${value.slice(0, 3)}-${value.slice(3, 5)}-${value.slice(5)}`; + else if (value.length > 3) value = `${value.slice(0, 3)}-${value.slice(3)}`; setBusinessNumber(value); - // 입력 시 에러 메시지 초기화 setValidationError(""); }; - const [isOpen, setIsOpen] = useState(false); - - // 매장이 틀렸을 경우 버튼을 눌러서 다시 없어지도록 설정 - const closeShop = () => { - setIsOpen(false); - }; - - // 스탭별로 작업 - const [step, setStep] = useState(1); + const handleValidateBusinessNumber = async () => { + const cleanNumber = businessNumber.replace(/-/g, ""); - // 사업자등록번호 검증 함수 - const validateBusinessNumber = async (number: string) => { - // 하이픈 제거 - const cleanNumber = number.replace(/-/g, ''); - - // 검증 기본 규칙 - 10자리 숫자 확인 - if (!/^\d{10}$/.test(cleanNumber)) { + if (cleanNumber.length !== 10) { setValidationError("사업자등록번호는 10자리 숫자여야 합니다."); - return false; + return; } - + try { setIsValidating(true); - - // 백엔드 API를 통한 사업자번호 검증 - const response = await axiosInstance.post('/store/validate-business-number', { - businessNumber: cleanNumber - }); - - // 검증 결과 - const isValid = response.data; - - if (!isValid) { - setValidationError("유효하지 않은 사업자등록번호입니다."); - } - - return isValid; + await validateBusinessNumber(cleanNumber); + alert("유효한 사업자등록번호입니다."); + setValidationError(""); } catch (error) { - console.error("사업자등록번호 검증 중 오류 발생:", error); - setValidationError("사업자등록번호 검증에 실패했습니다."); - return false; + console.error("사업자등록번호 검증 실패:", error); + setValidationError("유효하지 않은 사업자등록번호입니다."); } finally { setIsValidating(false); } }; - // 사업자번호로 매장 정보 조회 - const fetchFakeShop = async () => { - if (!businessNumber) { - setValidationError("사업자등록번호를 입력해주세요."); - return; - } - - // 하이픈 제거 - const cleanNumber = businessNumber.replace(/-/g, ''); - - // 사업자번호 검증 - const isValid = await validateBusinessNumber(businessNumber); - - if (!isValid) { - return; - } - - // 하드코딩된 데이터에서 매장 조회 - if (fakeShops[cleanNumber]) { - setShop(fakeShops[cleanNumber]); // 매장명 설정 - setIsOpen(true); - } else { - // 검증은 성공했지만 DB에 없는 사업자번호일 경우 매장명 입력 기능 추가 - setValidationError("확인된 사업자번호이나 등록된 매장 정보가 없습니다. 매장명을 직접 입력해주세요."); - setShop(""); - setIsOpen(true); - } - }; - - // 사업자번호에 따른 하드코딩된 매장 정보 - const fakeShops: { [key: string]: string } = { - "0000000000": "스타벅스 성신여대점", - "1111111111": "투썸플레이스 성공회대점", - "2222222222": "이디야 숙명여대점", - }; - - // 매장 정보 저장 - const registerShop = async () => { - if (!shop && !document.getElementById('shopNameInput')) { - setValidationError("매장명을 입력해주세요."); - return; - } - - // 직접 입력한 매장명 가져오기 - const inputElement = document.getElementById('shopNameInput') as HTMLInputElement; - const shopName = inputElement ? inputElement.value : shop; - - if (!shopName) { - setValidationError("매장명을 입력해주세요."); - return; - } - - try { - await axiosInstance.post("/store", { - businessNumber: businessNumber.replace(/-/g, ''), - shopName: shopName, - }); - - setStep(2); // 등록 완료 화면으로 이동 - } catch (error) { - console.error("매장 등록 중 오류 발생:", error); - setValidationError("매장 등록에 실패했습니다."); - } - }; - - // 매장명 직접 입력 상태 - const [isDirectInput, setIsDirectInput] = useState(false); - - // 매장명 직접 입력 전환 - const toggleDirectInput = () => { - setIsDirectInput(!isDirectInput); - if (!isDirectInput) { - setShop(""); - } - }; - return (
-
+
근무지 등록하기
-
- 취소 -
+
취소
@@ -186,81 +62,14 @@ const AlbaAddModal: React.FC = ({ onClose }) => { />
+ {validationError &&
{validationError}
}
- {validationError && ( -
{validationError}
- )} - - {/* 입력 확인을 눌렀을 때 매장이 나오도록 하는 코드 */} - {isOpen && ( - <> -
-
-
매장명
- {isDirectInput && ( -
- 저장된 매장명 사용 -
- )} - {!isDirectInput && shop && ( -
- 직접 입력 -
- )} -
-
- {isDirectInput || !shop ? ( - - ) : ( -
{shop}
- )} -
-
- - {/* 매장이 나오도록 하는 코드 */} - {/* 동시에 "위 매장이 맞나요?" 텍스트와 두 개의 버튼이 나타남 */} -
근무지가 위 매장이 맞나요?
- {step === 1 && ( -
-
- )} - - )} - {step === 2 && ( - <> -
-
매장등록이 완료되었습니다.
-
- 메인페이지로 이동하기. -
-
- - )}
From a394d4ffe6759610479f1a6cca2c8f845c8c6ad9 Mon Sep 17 00:00:00 2001 From: JeongHyeon <0429cjhn@naver.com> Date: Wed, 9 Jul 2025 21:38:53 +0900 Subject: [PATCH 02/13] =?UTF-8?q?feat:=20store=20=EC=82=AC=EC=9E=A5?= =?UTF-8?q?=EB=8B=98=20=EB=A7=A4=EC=9E=A5=20=EB=93=B1=EB=A1=9D=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/albaAdd.ts | 4 +- src/api/axios.ts | 38 ++++++++- src/components/Modal/AlbaAddModal.tsx | 102 ++++++++++++++++++++--- src/components/SelectRadio.tsx | 37 ++++---- src/contexts/EmployeeScheduleContext.tsx | 71 ++++++++-------- 5 files changed, 181 insertions(+), 71 deletions(-) diff --git a/src/api/albaAdd.ts b/src/api/albaAdd.ts index c494c6f..1aded6c 100644 --- a/src/api/albaAdd.ts +++ b/src/api/albaAdd.ts @@ -4,9 +4,6 @@ interface AddStoreRequest { name: string; location: string; businessNumber: string; - storeCode: string; - requireApproval: boolean; - defaultHourlyWage: number; } export const validateBusinessNumber = (businessNumber: string) => { @@ -17,5 +14,6 @@ export const validateBusinessNumber = (businessNumber: string) => { export const registerStore = (data: AddStoreRequest) => { + console.log("📤 [API] registerStore payload:", data); // ✅ 여기도 찍어볼 수 있음 return axiosInstance.post("/store", data); }; diff --git a/src/api/axios.ts b/src/api/axios.ts index bfb8117..e2ffec8 100644 --- a/src/api/axios.ts +++ b/src/api/axios.ts @@ -2,20 +2,50 @@ import axios from "axios"; const axiosInstance = axios.create({ - baseURL: import.meta.env.VITE_API_URL, + baseURL: import.meta.env.VITE_API_URL, // .env에서 설정된 API 주소 headers: { "Content-Type": "application/json", }, - withCredentials: true, + withCredentials: true, // 필요 시 }); -// api 요청 실패 시 공통 에러 처리 등을 추가 +// ✅ 요청 인터셉터 - Authorization 토큰 자동 삽입 +axiosInstance.interceptors.request.use( + (config) => { + const token = localStorage.getItem("accessToken"); + console.log("🔐 저장된 토큰:", token); + + if (token) { + config.headers["Authorization"] = `Bearer ${token}`; + console.log("✅ Authorization 헤더 추가됨:", config.headers); + } else { + console.warn("⚠️ accessToken이 없습니다. 로그인되지 않은 상태입니다."); + } + + return config; + }, + (error) => Promise.reject(error) +); + +// ✅ 응답 인터셉터 - 공통 에러 처리 (선택) axiosInstance.interceptors.response.use( (response) => response, (error) => { - // 에러 처리 로직 + // 여기에 401, 403 등 처리 가능 return Promise.reject(error); } ); +// ✅ 다른 탭에서 로그인/로그아웃 시 헤더 자동 반영 +window.addEventListener("storage", () => { + const token = localStorage.getItem("accessToken"); + if (token) { + axiosInstance.defaults.headers["Authorization"] = `Bearer ${token}`; + console.log("✅ 스토리지 변경 감지: Authorization 헤더 업데이트됨!"); + } else { + delete axiosInstance.defaults.headers["Authorization"]; + console.log("🧼 accessToken 삭제됨: Authorization 헤더 제거됨."); + } +}); + export default axiosInstance; diff --git a/src/components/Modal/AlbaAddModal.tsx b/src/components/Modal/AlbaAddModal.tsx index 2f8891f..f603963 100644 --- a/src/components/Modal/AlbaAddModal.tsx +++ b/src/components/Modal/AlbaAddModal.tsx @@ -1,39 +1,41 @@ import styles from "./AlbaAddModal.module.css"; import { useState } from "react"; import Button from "../Button"; -import { validateBusinessNumber } from "../../api/albaAdd"; +import { validateBusinessNumber, registerStore } from "../../api/albaAdd"; interface AlbaAddModalProps { onClose: () => void; } const AlbaAddModal: React.FC = ({ onClose }) => { + const [step, setStep] = useState(1); + const [businessNumber, setBusinessNumber] = useState(""); + const [storeName, setStoreName] = useState(""); + const [location, setLocation] = useState(""); + const [defaultHourlyWage, setDefaultHourlyWage] = useState(0); + const [isValidating, setIsValidating] = useState(false); const [validationError, setValidationError] = useState(""); const handleBusinessNumber = (e: React.ChangeEvent) => { - let value = e.target.value.replace(/[^\d]/g, ""); - if (value.length > 10) value = value.slice(0, 10); - if (value.length > 5) value = `${value.slice(0, 3)}-${value.slice(3, 5)}-${value.slice(5)}`; - else if (value.length > 3) value = `${value.slice(0, 3)}-${value.slice(3)}`; + const value = e.target.value.replace(/[^\d]/g, ""); // 숫자만 허용, 하이픈 제거 setBusinessNumber(value); setValidationError(""); }; const handleValidateBusinessNumber = async () => { - const cleanNumber = businessNumber.replace(/-/g, ""); - - if (cleanNumber.length !== 10) { + if (businessNumber.length !== 10) { setValidationError("사업자등록번호는 10자리 숫자여야 합니다."); return; } try { setIsValidating(true); - await validateBusinessNumber(cleanNumber); + await validateBusinessNumber(businessNumber); alert("유효한 사업자등록번호입니다."); setValidationError(""); + setStep(2); // step 2로 이동 } catch (error) { console.error("사업자등록번호 검증 실패:", error); setValidationError("유효하지 않은 사업자등록번호입니다."); @@ -42,6 +44,34 @@ const AlbaAddModal: React.FC = ({ onClose }) => { } }; + const handleRegisterStore = async () => { + if (!storeName || !location || !defaultHourlyWage) { + setValidationError("모든 값을 입력해주세요."); + return; + } + + const payload = { + name: storeName, + location: location, + businessNumber: businessNumber, + defaultHourlyWage: defaultHourlyWage, + }; + + console.log("📦 registerStore로 보낼 데이터:", payload); + + try { + await registerStore(payload); + alert("매장이 성공적으로 등록되었습니다."); + onClose(); + } catch (error: any) { + console.error("매장 등록 실패:", error); + if (error.response?.data) { + console.error("❗서버 응답 메시지:", error.response.data); + } + setValidationError("매장 등록 중 오류가 발생했습니다."); + } + }; + return (
@@ -49,7 +79,9 @@ const AlbaAddModal: React.FC = ({ onClose }) => {
근무지 등록하기
취소
+
+ {/* Step 1: 사업자등록번호 입력 */}
매장 사업자번호 입력
@@ -58,7 +90,7 @@ const AlbaAddModal: React.FC = ({ onClose }) => { value={businessNumber} onChange={handleBusinessNumber} className={styles.numberInput} - placeholder="매장의 사업자번호를 입력해 주세요 (000-00-00000)" + placeholder="매장의 사업자번호를 입력해 주세요 (숫자만 입력)" />
{validationError &&
{validationError}
}
+ + {/* Step 2: 매장 정보 입력 */} + {step === 2 && ( + <> +
+
매장 이름
+
+ setStoreName(e.target.value)} + className={styles.numberInput} + placeholder="매장 이름을 입력해 주세요" + /> +
+
+ +
+
매장 위치
+
+ setLocation(e.target.value)} + className={styles.numberInput} + placeholder="매장 위치를 입력해 주세요 (예: 서울 송파구)" + /> +
+
+ +
+
기본 시급
+
+ setDefaultHourlyWage(Number(e.target.value))} + className={styles.numberInput} + placeholder="기본 시급을 입력해 주세요 (예: 9860)" + /> +
+
+ +
+ +
+ + )}
diff --git a/src/components/SelectRadio.tsx b/src/components/SelectRadio.tsx index efc0eb4..3b34a0e 100644 --- a/src/components/SelectRadio.tsx +++ b/src/components/SelectRadio.tsx @@ -12,25 +12,28 @@ const SelectRadio = () => {
{safeStores.length > 0 ? ( - safeStores.map((store) => ( - + ); + }) ) : (

가게 목록이 없습니다.

)} diff --git a/src/contexts/EmployeeScheduleContext.tsx b/src/contexts/EmployeeScheduleContext.tsx index 5416bda..c6c0236 100644 --- a/src/contexts/EmployeeScheduleContext.tsx +++ b/src/contexts/EmployeeScheduleContext.tsx @@ -22,11 +22,17 @@ interface EmployeeSchedule { } interface Store { + id: number | null; storeId: number; - name: string; storeCode: string; + businessNumber: string | null; + name: string; + location: string; + requireApproval: boolean | null; + createdAt: string; } + /* 공유할 context type */ interface EmployeeScheduleContextType { stores: Store[]; // 전체 매장 목록 -> 나중에 사용자 따라서 다른 걸 받아오도록 해야 됨! @@ -94,48 +100,39 @@ export const EmployeeScheduleProvider = ({ // DB에서 가게 목록 가져오기 (axios로 변경) useEffect(() => { - const fetchStores = async () => { - try { - if (token) { - // axios 요청 헤더에 토큰을 추가합니다. - const res = await axiosInstance.get( - "http://43.200.176.79:8080/store/me", - { - headers: { - Authorization: `Bearer ${token}`, // Authorization 헤더에 Bearer 토큰 추가 - }, - } - ); - const data = res.data; - - // console.log("받아온 데이터: ", data); - - if (!data) { - console.error("가게 목록 데이터가 없습니다."); - return; + const fetchStores = async () => { + try { + if (token) { + const res = await axiosInstance.get( + "http://43.200.176.79:8080/store/me", + { + headers: { + Authorization: `Bearer ${token}`, + }, } + ); + const data = res.data; - const filteredStores = data.map((store: Store) => ({ - storeId: store.storeId, - name: store.name, - storeCode: store.storeCode, - })); + if (!data) { + console.error("가게 목록 데이터가 없습니다."); + return; + } - setStores(filteredStores); - if (filteredStores.length > 0) { - setSelectedStore(filteredStores[0].storeId); - } - } else { - console.error("토큰이 없습니다. 인증을 확인하세요."); - // 인증 절차를 여기에 추가할 수 있습니다. + setStores(data); // ✅ 전체 저장 + if (data.length > 0) { + setSelectedStore(data[0].storeId); // ✅ 첫 가게 자동 선택 } - } catch (error) { - console.error("가게 목록을 불러오는 데 실패했습니다.", error); + } else { + console.error("토큰이 없습니다. 인증을 확인하세요."); } - }; + } catch (error) { + console.error("가게 목록을 불러오는 데 실패했습니다.", error); + } + }; + + fetchStores(); +}, [token]); - fetchStores(); - }, [token]); // 가게 선택할 때마다 스케줄 다르게 불러오기 (axios로 변경) useEffect(() => { From 56822c92e44ae084ab0c29b662ad9ed5803718b2 Mon Sep 17 00:00:00 2001 From: JeongHyeon <0429cjhn@naver.com> Date: Thu, 10 Jul 2025 17:37:28 +0900 Subject: [PATCH 03/13] =?UTF-8?q?feat:=20=EC=95=8C=EB=B0=94=EC=83=9D=20?= =?UTF-8?q?=EA=B7=BC=EB=AC=B4=EC=A7=80=20=EA=B0=80=EC=9E=85=20=EB=AA=A8?= =?UTF-8?q?=EB=8B=AC=20=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Pages/Employee/EmployeeMainPage.tsx | 4 +- src/api/albaAdd.ts | 6 +++ src/components/Modal/AlbaJoinModal.tsx | 59 ++++++++++++++++++++++++ src/contexts/EmployeeScheduleContext.tsx | 11 +++-- 4 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 src/components/Modal/AlbaJoinModal.tsx diff --git a/src/Pages/Employee/EmployeeMainPage.tsx b/src/Pages/Employee/EmployeeMainPage.tsx index 1393f3f..6a9463e 100644 --- a/src/Pages/Employee/EmployeeMainPage.tsx +++ b/src/Pages/Employee/EmployeeMainPage.tsx @@ -5,7 +5,7 @@ import CalendarEmployee from "../../components/CalendarEmployee"; import ChoiceEmployee from "../../components/ChoiceEmployee"; import PartTimeEmployee from "../../components/PartTimeEmployee"; import SelectRadioEmployee from "../../components/SelectRadioEmployee"; -import AlbaAddModal from "../../components/Modal/AlbaAddModal"; +import AlbaJoinModal from "../../components/Modal/AlbaJoinModal"; import AlarmModal from "../../components/Modal/AlarmModal"; import RequestModalEmployee from "../../components/Modal/RequestModalEmployee"; import styles from "./EmployeeMainPage.module.css"; @@ -39,7 +39,7 @@ const EmployeeMainPage = () => {
{/* 모달 조건부 렌더링 */} {activeModal === "alarm" && } - {activeModal === "albaAdd" && } + {activeModal === "albaAdd" && } {activeModal === "request" && }
); diff --git a/src/api/albaAdd.ts b/src/api/albaAdd.ts index 1aded6c..e69e073 100644 --- a/src/api/albaAdd.ts +++ b/src/api/albaAdd.ts @@ -17,3 +17,9 @@ export const registerStore = (data: AddStoreRequest) => { console.log("📤 [API] registerStore payload:", data); // ✅ 여기도 찍어볼 수 있음 return axiosInstance.post("/store", data); }; + +// 알바생이 매장 등록 +export const joinStore = (storeCode: string) => { + console.log("📤 [API] joinStore payload:", storeCode); + return axiosInstance.post("/store/join", { storeCode }); +}; diff --git a/src/components/Modal/AlbaJoinModal.tsx b/src/components/Modal/AlbaJoinModal.tsx new file mode 100644 index 0000000..d290e83 --- /dev/null +++ b/src/components/Modal/AlbaJoinModal.tsx @@ -0,0 +1,59 @@ +import styles from "./AlbaAddModal.module.css"; +import { useState } from "react"; +import Button from "../Button"; +import { joinStore } from "../../api/albaAdd"; +import { useEmployeeSchedule } from "../../contexts/EmployeeScheduleContext"; // ✅ 추가 + +interface AlbaJoinModalProps { + onClose: () => void; +} + +const AlbaJoinModal: React.FC = ({ onClose }) => { + const [storeCode, setStoreCode] = useState(""); + + const { refetchStores } = useEmployeeSchedule(); // ✅ 추가 + + const handleJoinStore = async () => { + try { + const res = await joinStore(storeCode); // 참여 코드로 매장 등록 요청 + alert("매장에 성공적으로 참여했어요!"); + await refetchStores(); // ✅ 매장 목록 다시 불러오기 + onClose(); // 모달 닫기 + } catch (err: any) { + console.error("❌ 참여 실패", err.response?.data || err.message); + alert(err.response?.data?.message || "유효하지 않은 참여 코드입니다."); + } + }; + + return ( +
+
+
+
근무지 추가하기
+
취소
+
+ +
+
+
참여 코드 입력
+
+ setStoreCode(e.target.value)} + className={styles.numberInput} + placeholder="매장 참여 코드를 입력해 주세요" + /> +
+ + +
+
+
+
+ ); +}; + +export default AlbaJoinModal; diff --git a/src/contexts/EmployeeScheduleContext.tsx b/src/contexts/EmployeeScheduleContext.tsx index c6c0236..e714894 100644 --- a/src/contexts/EmployeeScheduleContext.tsx +++ b/src/contexts/EmployeeScheduleContext.tsx @@ -70,6 +70,7 @@ interface EmployeeScheduleContextType { selectedName: string; setSelectedName: React.Dispatch>; otherGroupMembers: { userId: number; name: string }[]; + refetchStores: () => Promise; // ✅ 이 줄 추가! } const EmployeeScheduleContext = createContext< @@ -98,8 +99,6 @@ export const EmployeeScheduleProvider = ({ // console.log("selectedName: ", selectedName); - // DB에서 가게 목록 가져오기 (axios로 변경) - useEffect(() => { const fetchStores = async () => { try { if (token) { @@ -129,9 +128,10 @@ export const EmployeeScheduleProvider = ({ console.error("가게 목록을 불러오는 데 실패했습니다.", error); } }; - - fetchStores(); -}, [token]); + // DB에서 가게 목록 가져오기 (axios로 변경) + useEffect(() => { + fetchStores(); + }, [token]); // 가게 선택할 때마다 스케줄 다르게 불러오기 (axios로 변경) @@ -456,6 +456,7 @@ export const EmployeeScheduleProvider = ({ selectedName, setSelectedName, otherGroupMembers, + refetchStores: fetchStores, // ✅ 이 한 줄 추가! }}> {children} From 5a48377932677a07e4f140807a69d9fa4ac0858f Mon Sep 17 00:00:00 2001 From: JeongHyeon <0429cjhn@naver.com> Date: Fri, 11 Jul 2025 16:57:32 +0900 Subject: [PATCH 04/13] =?UTF-8?q?feat:=20=EC=95=8C=EB=B0=94=EC=83=9D=20?= =?UTF-8?q?=EB=A7=A4=EC=9E=A5=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/albaAdd.ts | 5 + .../Modal/Edit/DeleteStoreModal.module.css | 72 ++++++++ .../Modal/Edit/DeleteStoreModal.tsx | 53 ++++++ .../Modal/Edit/EditableField.module.css | 7 + src/components/Modal/Edit/EditableField.tsx | 14 +- .../employeeMy/EmployeeMyInfo.module.css | 29 ++++ src/components/employeeMy/EmployeeMyInfo.tsx | 164 +++++++++++------- src/contexts/EmployeeScheduleContext.tsx | 2 + 8 files changed, 278 insertions(+), 68 deletions(-) create mode 100644 src/components/Modal/Edit/DeleteStoreModal.module.css create mode 100644 src/components/Modal/Edit/DeleteStoreModal.tsx diff --git a/src/api/albaAdd.ts b/src/api/albaAdd.ts index e69e073..ec6338a 100644 --- a/src/api/albaAdd.ts +++ b/src/api/albaAdd.ts @@ -23,3 +23,8 @@ export const joinStore = (storeCode: string) => { console.log("📤 [API] joinStore payload:", storeCode); return axiosInstance.post("/store/join", { storeCode }); }; + +// 매장 삭제 +export const deleteStore = (storeId: number) => { + return axiosInstance.delete(`/store/${storeId}`); +}; diff --git a/src/components/Modal/Edit/DeleteStoreModal.module.css b/src/components/Modal/Edit/DeleteStoreModal.module.css new file mode 100644 index 0000000..fd906be --- /dev/null +++ b/src/components/Modal/Edit/DeleteStoreModal.module.css @@ -0,0 +1,72 @@ +.modalOverlay { + width: 100vw; + height: 100vh; + position: fixed; + top: 0%; + left: 50%; + transform: translateX(-50%); /* ✅ 좌우 정중앙 정렬 */ + + background-color: rgba(0, 0, 0, 0.5); + z-index: 10; + + font-family: "Noto Sans KR", sans-serif; + + display: flex; + justify-content: center; + /* align-items: flex-start; */ +} + +.modal { + width: 1000px; + height: 400px; + /* border: 1px black solid; */ + border-radius: 30px; + background-color: white; + margin-top: 250px; +} + +.container { + width: 900px; + height: 40px; + border-bottom: 1px #666666 solid; + margin: 0 auto; + display: flex; + justify-content: space-between; +} +.title { + width: 608px; + height: 24px; + /* border: 1px black solid; */ + margin-left: 20px; + font-size: 20px; + font-weight: 700; +} +.button { + width: 30px; + height: 24px; + /* border: 1px black solid; */ + margin-right: 20px; +} +.button:hover { + cursor: pointer; +} +.contentContainer { + display: flex; + align-items: center; + justify-content: center; + + flex-direction: column; + gap: 70px; +} +.contents { + width: 750px; + margin-top: 10%; + + border: 1px solid black; + + display: flex; + justify-content: center +} +.input { + width: 450px; +} diff --git a/src/components/Modal/Edit/DeleteStoreModal.tsx b/src/components/Modal/Edit/DeleteStoreModal.tsx new file mode 100644 index 0000000..bebf0a9 --- /dev/null +++ b/src/components/Modal/Edit/DeleteStoreModal.tsx @@ -0,0 +1,53 @@ +import { useState } from "react"; +import styles from "./DeleteStoreModal.module.css"; +import { deleteStore } from "../../../api/albaAdd"; // 경로는 실제 위치에 맞게 조절 + +interface AlarmProps { + storeId: number; // ✅ 추가: 실제 API 호출용 + storeName: string; // ✅ 기존 그대로 유지 + onClose: () => void; + onSuccess: () => void; +} + +const DeleteStoreModal: React.FC = ({ + storeId, + storeName, + onClose, + onSuccess, +}) => { + const [isLoading, setIsLoading] = useState(false); + + const handleDelete = async () => { + try { + setIsLoading(true); + await deleteStore(storeId); // ✅ API 호출 + onSuccess(); // ✅ 성공 후 콜백 + } catch (error) { + console.error("❌ 매장 삭제 실패:", error); + alert("매장 삭제에 실패했습니다. 다시 시도해주세요."); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+
+
매장 삭제하기
+
취소
+
+
+
+ 정말 {storeName} 매장을 삭제하시겠습니까? +
+
+ {isLoading ? "삭제 중..." : "확인"} +
+
+
+
+ ); +}; + +export default DeleteStoreModal; diff --git a/src/components/Modal/Edit/EditableField.module.css b/src/components/Modal/Edit/EditableField.module.css index b577ea3..9f4015d 100644 --- a/src/components/Modal/Edit/EditableField.module.css +++ b/src/components/Modal/Edit/EditableField.module.css @@ -18,3 +18,10 @@ font-size: 20px; text-decoration: underline; } + +.manageButton { + color: #4a90e2; + cursor: pointer; + font-size: 14px; + font-weight: bold; +} diff --git a/src/components/Modal/Edit/EditableField.tsx b/src/components/Modal/Edit/EditableField.tsx index b683a0d..6cdb594 100644 --- a/src/components/Modal/Edit/EditableField.tsx +++ b/src/components/Modal/Edit/EditableField.tsx @@ -1,14 +1,16 @@ // ✅ EditableField: 사용자 정보(이름, 이메일, 비밀번호 등)를 화면에 출력하고, // 편집 모드일 때, 수정 가능한 항목만 클릭 가능하게 만드는 재사용 컴포넌트 import styles from "./EditableField.module.css"; +import { ReactNode } from "react"; interface EditableFieldProps { label: string; // 항목 라벨 (예: 이름) - value: string; // 항목 값 (예: 홍길동) + value?: string; // 항목 값 (예: 홍길동) (선택) fieldName: string; // 필드 이름 (예: "fullName", "email") isEditing: boolean; // 현재 편집 모드인지 여부 isEditable: boolean; // 해당 항목이 수정 가능한지 여부 onClick?: (field: string) => void; // 클릭 이벤트 핸들러 (옵션) + children?: ReactNode; // 커스텀 출력용 JSX 지원 } const EditableField = ({ @@ -18,17 +20,21 @@ const EditableField = ({ isEditing, isEditable, onClick, + children, }: EditableFieldProps) => { const isClickable = isEditing && isEditable; + const isStoreField = fieldName === "storeNames"; // ✅ storeNames인지 확인 - return ( + return (
{label}
onClick(fieldName) : undefined} + onClick={ + isClickable && !isStoreField && onClick ? () => onClick(fieldName) : undefined + } > - {value || `${label} 없음`} + {children ?? value ?? `${label} 없음`}
); diff --git a/src/components/employeeMy/EmployeeMyInfo.module.css b/src/components/employeeMy/EmployeeMyInfo.module.css index c3fa517..3e37748 100644 --- a/src/components/employeeMy/EmployeeMyInfo.module.css +++ b/src/components/employeeMy/EmployeeMyInfo.module.css @@ -52,3 +52,32 @@ font-size: 20px; text-decoration: underline; } + +.storeList { + display: flex; + flex-direction: column; + gap: 10px; +} + +.storeItem { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 20px; + color: black; +} + +.storeName { + flex: 1; + word-break: keep-all; + text-decoration: none; /* ✅ 밑줄 제거 */ +} + +.manageButton { + margin-left: 12px; + color: #4a90e2; + cursor: pointer; + font-size: 20px; + white-space: nowrap; + text-decoration: underline; /* ✅ 밑줄 유지 */ +} diff --git a/src/components/employeeMy/EmployeeMyInfo.tsx b/src/components/employeeMy/EmployeeMyInfo.tsx index ff0ce34..549a298 100644 --- a/src/components/employeeMy/EmployeeMyInfo.tsx +++ b/src/components/employeeMy/EmployeeMyInfo.tsx @@ -3,21 +3,23 @@ import { useState, useEffect } from "react"; import { useModal } from "../../contexts/ModalContext"; import EditModal from "../Modal/Edit/EditModal"; import DoneModal from "../Modal/Edit/DoneModal"; -import axiosInstance from "../../api/loginAxios" -import EditableField from "../../components/Modal/Edit/EditableField" -import EditNameModal from "../Modal/Edit/EditNameModal"; -import EditEmailModal from "../Modal/Edit/EditEmailModal" -import EditPasswordModal from "../Modal/Edit/EditPasswordModal" -import EditStoreModal from "../Modal/Edit/EditStoreModal"; +import axiosInstance from "../../api/loginAxios"; +import EditableField from "../../components/Modal/Edit/EditableField"; +import EditNameModal from "../Modal/Edit/EditNameModal"; +import EditEmailModal from "../Modal/Edit/EditEmailModal"; +import EditPasswordModal from "../Modal/Edit/EditPasswordModal"; +import DeleteStoreModal from "../Modal/Edit/DeleteStoreModal"; const EmployeeMyInfo = () => { const { activeModal, openModal, closeModal } = useModal(); const [isEditing, setIsEditing] = useState(false); + const [storeToDelete, setStoreToDelete] = useState<{ id: number; name: string } | null>(null); const [isEditNameModalOpen, setIsEditNameModalOpen] = useState(false); const [isEditEmailModalOpen, setIsEditEmailModalOpen] = useState(false); const [isEditPasswordModalOpen, setIsEditPasswordModalOpen] = useState(false); const [isEditStoreModalOpen, setIsEditStoreModalOpen] = useState(false); + const handleEditField = (field: string) => { switch (field) { case "fullName": @@ -40,7 +42,7 @@ const EmployeeMyInfo = () => { email: "", password: "********", role: "", - storeNames: [] as string[], + stores: [] as { id: number; name: string }[], }); useEffect(() => { @@ -49,16 +51,24 @@ const EmployeeMyInfo = () => { const response = await axiosInstance.get("/user/me"); console.log("🔍 유저 정보:", response.data); + // user/me에는 storeNames(string[])만 있으므로 storeId가 필요한 경우 /store/me 추가 호출 필요 + const storeResponse = await axiosInstance.get("/store/me"); + const storeData = storeResponse.data || []; + + const formattedStores = storeData.map((store: any) => ({ + id: store.storeId, + name: store.name, + })); + setUserInfo({ fullName: response.data.fullName || "", email: response.data.email || "", - password: "********", // 실제 비번은 보여주지 않음 + password: "********", role: response.data.role || "", - storeNames: response.data.storeNames || [], - }); + stores: formattedStores, + }); } catch (error: any) { console.error("유저 정보 불러오기 실패:", error); - if (error.response?.status === 401) { localStorage.removeItem("accessToken"); window.location.href = "/login"; @@ -66,8 +76,8 @@ const EmployeeMyInfo = () => { } }; - fetchUserInfo(); -}, []); + fetchUserInfo(); + }, []); return (
@@ -76,17 +86,13 @@ const EmployeeMyInfo = () => {
{ - if (isEditing) { - openModal("done"); - console.log("수정 완료 버튼 클릭됨"); - } else { - openModal("edit"); // 비밀번호 확인 모달 열기 - } + isEditing ? openModal("done") : openModal("edit"); }} > {isEditing ? "수정 완료" : "내 정보 수정하기"}
+
{ isEditable={true} onClick={handleEditField} /> - + > +
+ {userInfo.stores.map((store) => ( +
+ {store.name} + {isEditing && ( + { + console.log("🧩 매장 관리 클릭됨:", store); + setStoreToDelete({ id: store.id, name: store.name }); + }} + > + 매장 관리 + + )} +
+ ))} +
+
- {/* activeModal이 "edit"일 때 EditModal 렌더링 */} - {activeModal === "edit" && setIsEditing(true)}/>} - {activeModal === "done" && setIsEditing(false)}/>} - {isEditNameModalOpen && ( - setIsEditNameModalOpen(false)} - onSave={(newFullName) => { - setUserInfo((prev) => ({ ...prev, fullName: newFullName })); - setIsEditNameModalOpen(false); - }} - /> - )} - {isEditEmailModalOpen && ( - setIsEditEmailModalOpen(false)} - onSuccess={(email) => { - setUserInfo((prev) => ({ ...prev, email: email })); - setIsEditEmailModalOpen(false); - }} - /> - )} - {isEditPasswordModalOpen && ( - setIsEditPasswordModalOpen(false)} - onSuccess={() => { - setUserInfo((prev) => ({ ...prev, password:"********" })); - setIsEditPasswordModalOpen(false); - }} - /> - )} - - {/* {isEditStoreModalOpen && ( - setIsEditStoreModalOpen(false)} - onSave={(storeNames) => { - setUserInfo((prev) => ({ ...prev, storeNames })); - setIsEditStoreModalOpen(false); - }} - /> - )} */} + {/* ✅ 모달들 */} + {activeModal === "edit" && ( + setIsEditing(true)} /> + )} + {activeModal === "done" && ( + setIsEditing(false)} /> + )} + {isEditNameModalOpen && ( + setIsEditNameModalOpen(false)} + onSave={(newFullName) => { + setUserInfo((prev) => ({ ...prev, fullName: newFullName })); + setIsEditNameModalOpen(false); + }} + /> + )} + + {isEditEmailModalOpen && ( + setIsEditEmailModalOpen(false)} + onSuccess={(email) => { + setUserInfo((prev) => ({ ...prev, email })); + setIsEditEmailModalOpen(false); + }} + /> + )} + + {isEditPasswordModalOpen && ( + setIsEditPasswordModalOpen(false)} + onSuccess={() => { + setUserInfo((prev) => ({ ...prev, password: "********" })); + setIsEditPasswordModalOpen(false); + }} + /> + )} + + {/* ✅ 삭제 모달 */} + {storeToDelete && ( + setStoreToDelete(null)} + onSuccess={() => { + setUserInfo((prev) => ({ + ...prev, + stores: prev.stores.filter((s) => s.id !== storeToDelete.id), + })); + setStoreToDelete(null); + // ❌ setIsEditing(false) 제거 => 수정모드 유지! + }} + /> + )} ); }; diff --git a/src/contexts/EmployeeScheduleContext.tsx b/src/contexts/EmployeeScheduleContext.tsx index e714894..d776030 100644 --- a/src/contexts/EmployeeScheduleContext.tsx +++ b/src/contexts/EmployeeScheduleContext.tsx @@ -111,6 +111,8 @@ export const EmployeeScheduleProvider = ({ } ); const data = res.data; + + console.log("📦 불러온 매장 데이터:", data); // ✅ 이 줄 추가 if (!data) { console.error("가게 목록 데이터가 없습니다."); From 1918b04c668f11d086e7d437ff580cd3e61b2862 Mon Sep 17 00:00:00 2001 From: JeongHyeon <0429cjhn@naver.com> Date: Sun, 20 Jul 2025 23:54:36 +0900 Subject: [PATCH 05/13] =?UTF-8?q?feat:=20=EC=82=AC=EC=9E=A5=EB=8B=98=20?= =?UTF-8?q?=EB=A7=A4=EC=9E=A5=EC=97=90=EC=84=9C=20=EC=95=8C=EB=B0=94?= =?UTF-8?q?=EC=83=9D=20=EC=82=AD=EC=A0=9C=20=EA=B8=B0=EB=8A=A5=20=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/App.tsx | 10 +- src/Pages/Owner/OwnerMyPage.tsx | 47 ++-- src/Pages/Owner/my/Header.tsx | 64 ++++++ src/Pages/Owner/my/Navbar.tsx | 22 ++ src/Pages/Owner/my/OwnerMyInfo.tsx | 215 +++++++++++++++++++ src/Pages/Owner/my/StoreEditModal.module.css | 121 +++++++++++ src/Pages/Owner/my/StoreEditModal.tsx | 149 +++++++++++++ src/api/albaAdd.ts | 48 ++++- 8 files changed, 635 insertions(+), 41 deletions(-) create mode 100644 src/Pages/Owner/my/Header.tsx create mode 100644 src/Pages/Owner/my/Navbar.tsx create mode 100644 src/Pages/Owner/my/OwnerMyInfo.tsx create mode 100644 src/Pages/Owner/my/StoreEditModal.module.css create mode 100644 src/Pages/Owner/my/StoreEditModal.tsx diff --git a/src/App.tsx b/src/App.tsx index 74a8d9f..866c67e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,14 +35,8 @@ function App() { }> }> }> - } - > - } - > + }> + }>