From 76487ef9fcb1d8a934c8d1831110fe8949700bf9 Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:53:49 +0900 Subject: [PATCH 01/18] =?UTF-8?q?chore:=20.env=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index d4decf5..0000000 --- a/.env +++ /dev/null @@ -1 +0,0 @@ -REACT_APP_API_URL=https://dxdfx2l9z0ka8.cloudfront.net \ No newline at end of file From c1399dcce7a5ad2374e414814546dc7c4979acb5 Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:53:58 +0900 Subject: [PATCH 02/18] =?UTF-8?q?chore:=20=ED=99=98=EA=B2=BD=20=EB=B3=80?= =?UTF-8?q?=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index fe6ae1a..52ead39 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ yarn-debug.log* yarn-error.log* # env +.env.development +.env.production .env From 11176f2f966a99791eccf1bbe7fab0e1441cde06 Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:54:44 +0900 Subject: [PATCH 03/18] =?UTF-8?q?chore:=20=ED=8C=80=20=EC=86=8C=EA=B0=9C?= =?UTF-8?q?=20=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/AdminManagement.js | 272 +++++++++++++++++++++--------- src/pages/Info.js | 39 +++-- 2 files changed, 223 insertions(+), 88 deletions(-) diff --git a/src/components/AdminManagement.js b/src/components/AdminManagement.js index 9fb9cc7..11c8053 100644 --- a/src/components/AdminManagement.js +++ b/src/components/AdminManagement.js @@ -1,11 +1,12 @@ import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { jwtDecode } from "jwt-decode"; import "../styles/AdminManagement.css"; +const API_BASE_URL = `${process.env.REACT_APP_API_URL}/api/admin`; + const AdminManagement = () => { - const [admins, setAdmins] = useState([ - { email: "admin1@example.com", role: "SUPER" }, - { email: "user1@example.com", role: "NORMAL" }, - ]); + const [admins, setAdmins] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [selectedAdmin, setSelectedAdmin] = useState(null); @@ -16,64 +17,177 @@ const AdminManagement = () => { }); const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); const [adminToDelete, setAdminToDelete] = useState(null); - const [currentUser, setCurrentUser] = useState(null); + const [isAuthorized, setIsAuthorized] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + const [totalPages, setTotalPages] = useState(1); + const itemsPerPage = 10; useEffect(() => { - const storedUser = JSON.parse(localStorage.getItem("user")); - if (storedUser) { - setCurrentUser(storedUser); + const token = localStorage.getItem("token"); + if (!token) { + setIsAuthorized(false); + return; + } + + try { + const decodedToken = jwtDecode(token.replace("Bearer ", "").trim()); + console.log("πŸ”‘ ν˜„μž¬ λ‘œκ·ΈμΈν•œ κ΄€λ¦¬μž 정보:", decodedToken); + + if (decodedToken.role !== "SUPER") { + setIsAuthorized(false); + } + } catch (error) { + console.error("❌ 토큰 λ””μ½”λ”© μ‹€νŒ¨:", error); + localStorage.removeItem("token"); + setIsAuthorized(false); } }, []); - if (!currentUser || currentUser.role !== "SUPER") { + useEffect(() => { + if (!isAuthorized) return; + fetchAdmins(currentPage); + }, [isAuthorized, currentPage]); + + const fetchAdmins = async (page) => { + try { + const token = localStorage.getItem("token").replace("Bearer ", "").trim(); + const response = await axios.get( + `${API_BASE_URL}/list/page?page=${page - 1}&size=${itemsPerPage}`, + { + headers: { Authorization: `Bearer ${token}` }, + } + ); + + setAdmins(response.data.content); + setTotalPages(response.data.totalPages); + } catch (error) { + console.error("❌ κ΄€λ¦¬μž λͺ©λ‘ 뢈러였기 μ‹€νŒ¨:", error); + } + }; + + const fetchAdminByEmail = async (email) => { + try { + const token = localStorage.getItem("token").replace("Bearer ", "").trim(); + const response = await axios.get( + `${API_BASE_URL}/search?email=${email}`, + { + headers: { Authorization: `Bearer ${token}` }, + } + ); + + if (response.data) { + setAdmins([response.data]); + setTotalPages(1); + } + } catch (error) { + console.error("❌ κ΄€λ¦¬μž 검색 μ‹€νŒ¨:", error); + alert("ν•΄λ‹Ή μ΄λ©”μΌμ˜ κ΄€λ¦¬μžλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + }; + + const handleSearch = () => { + if (searchTerm.trim()) { + fetchAdminByEmail(searchTerm.trim()); + } else { + fetchAdmins(1); + } + }; + + const handleKeyPress = (e) => { + if (e.key === "Enter") { + handleSearch(); + } + }; + + const handlePageChange = (page) => { + setCurrentPage(page); + }; + + const closeModal = () => { + setIsEditModalOpen(false); + setIsDeleteModalOpen(false); + setSelectedAdmin(null); + setAdminToDelete(null); + }; + + if (!isAuthorized) { return (

⚠️ μ ‘κ·Ό κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.

+

일반 κ΄€λ¦¬μžλŠ” 이 νŽ˜μ΄μ§€μ— μ ‘κ·Όν•  수 μžˆλŠ” κΆŒν•œμ΄ μ—†μŠ΅λ‹ˆλ‹€.

); } - const handleAddAdmin = (e) => { + const handleDeleteAdmin = async () => { + try { + const token = localStorage.getItem("token").replace("Bearer ", "").trim(); + await axios.delete(`${API_BASE_URL}/${adminToDelete}`, { + headers: { Authorization: `Bearer ${token}` }, + }); + + alert("βœ… κ΄€λ¦¬μž μ‚­μ œ μ™„λ£Œ!"); + closeModal(); + + const response = await axios.get(`${API_BASE_URL}/list`, { + headers: { Authorization: `Bearer ${token}` }, + }); + setAdmins(response.data); + } catch (error) { + console.error("❌ κ΄€λ¦¬μž μ‚­μ œ μ‹€νŒ¨:", error); + alert("κ΄€λ¦¬μž μ‚­μ œμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); + } + }; + + const handleAddAdmin = async (e) => { e.preventDefault(); if (!newAdmin.email || !newAdmin.password) { alert("λͺ¨λ“  ν•„λ“œλ₯Ό μž…λ ₯ν•˜μ„Έμš”!"); return; } - if (admins.find((admin) => admin.email === newAdmin.email)) { - alert("이미 μ‘΄μž¬ν•˜λŠ” κ΄€λ¦¬μž μ΄λ©”μΌμž…λ‹ˆλ‹€."); - return; - } - setAdmins([...admins, newAdmin]); - setNewAdmin({ email: "", password: "", role: "NORMAL" }); - }; - const handleChangeRole = (admin) => { - setSelectedAdmin(admin); - setIsEditModalOpen(true); - }; + try { + const token = localStorage.getItem("token").replace("Bearer ", "").trim(); + await axios.post(`${API_BASE_URL}/register`, newAdmin, { + headers: { Authorization: `Bearer ${token}` }, + }); - const handleSaveRoleChange = () => { - setAdmins( - admins.map((admin) => - admin.email === selectedAdmin.email ? selectedAdmin : admin - ) - ); - setIsEditModalOpen(false); - }; + alert("βœ… κ΄€λ¦¬μž μΆ”κ°€ μ™„λ£Œ!"); + setNewAdmin({ email: "", password: "", role: "NORMAL" }); - const handleConfirmDelete = (email) => { - setAdminToDelete(email); - setIsDeleteModalOpen(true); + const response = await axios.get(`${API_BASE_URL}/list`, { + headers: { Authorization: `Bearer ${token}` }, + }); + setAdmins(response.data); + closeModal(); + } catch (error) { + console.error("❌ κ΄€λ¦¬μž μΆ”κ°€ μ‹€νŒ¨:", error); + alert(error.response?.data?.message || "κ΄€λ¦¬μž 좔가에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); + } }; - const handleDeleteAdmin = () => { - setAdmins(admins.filter((admin) => admin.email !== adminToDelete)); - setIsDeleteModalOpen(false); - }; + const handleSaveRoleChange = async (e) => { + e.preventDefault(); + try { + const token = localStorage.getItem("token").replace("Bearer ", "").trim(); + await axios.patch( + `${API_BASE_URL}/${selectedAdmin.id}/role`, + { role: selectedAdmin.role }, + { headers: { Authorization: `Bearer ${token}` } } + ); - const filteredAdmins = admins.filter((admin) => - admin.email.toLowerCase().includes(searchTerm.toLowerCase()) - ); + alert("κ΄€λ¦¬μž κΆŒν•œ λ³€κ²½ 성곡!"); + closeModal(); + + const response = await axios.get(`${API_BASE_URL}/list`, { + headers: { Authorization: `Bearer ${token}` }, + }); + setAdmins(response.data); + } catch (error) { + console.error("❌ κ΄€λ¦¬μž κΆŒν•œ λ³€κ²½ μ‹€νŒ¨:", error); + alert("κΆŒν•œ 변경에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); + } + }; return (
@@ -84,37 +198,40 @@ const AdminManagement = () => { placeholder="κ΄€λ¦¬μž 이메일 검색" value={searchTerm} onChange={(e) => setSearchTerm(e.target.value)} + onKeyPress={handleKeyPress} /> - +
- {/* κ΄€λ¦¬μž λͺ©λ‘ */}

κ΄€λ¦¬μž λͺ©λ‘

- {filteredAdmins.map((admin) => ( -
+ {admins.map((admin) => ( +

{admin.email} ( {admin.role === "SUPER" ? "졜고 κ΄€λ¦¬μž" : "일반 κ΄€λ¦¬μž"})

{admin.role !== "SUPER" && (
- {/* κ΄€λ¦¬μž μΆ”κ°€ / κΆŒν•œ λ³€κ²½ λͺ¨λ‹¬ */} + {totalPages > 1 && ( +
+ {[...Array(totalPages)].map((_, index) => ( + + ))} +
+ )} + {isEditModalOpen && (
+ +

{selectedAdmin ? "κΆŒν•œ λ³€κ²½" : "κ΄€λ¦¬μž μΆ”κ°€"}

{ selectedAdmin @@ -155,7 +288,6 @@ const AdminManagement = () => { setNewAdmin({ ...newAdmin, password: e.target.value }) @@ -181,42 +313,28 @@ const AdminManagement = () => { -
- - -
+
)} - {/* μ‚­μ œ 확인 λͺ¨λ‹¬ */} {isDeleteModalOpen && ( -
-
-

κ΄€λ¦¬μž μ‚­μ œ

-

ν•΄λ‹Ή κ΄€λ¦¬μžλ₯Ό μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?

+
+
+

정말 μ‚­μ œν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?

- +
diff --git a/src/pages/Info.js b/src/pages/Info.js index 832aa5e..d72c092 100644 --- a/src/pages/Info.js +++ b/src/pages/Info.js @@ -26,6 +26,23 @@ const Info = () => {
+
+

✏️ λͺ©ν‘œ

+
    +
  • + πŸ”Ή μ„œλΉ„μŠ€λ₯Ό μš΄μ˜ν•˜λŠ” λ‹¨μ²΄λ‚˜ κΈ°μ—…μ—μ„œ 고객을 관리할 수 μžˆλŠ” μ„œλΉ„μŠ€ + 개발. +
  • +
  • πŸ”Ή 카카였 지도 API의 κΈ°λŠ₯ 쀑 μ„œλΉ„μŠ€μ— μ–΄μšΈλ¦¬λŠ” κΈ°λŠ₯ μ„ μ •.
  • +
  • + πŸ”Ή 카카였 지도 API의 κΈ°λŠ₯κ³Ό ν•΄λ‹Ή κΈ°λŠ₯을 ν™œμš©ν•˜μ—¬ μΆ”μΆœν•œ 정보λ₯Ό + μ΅œλŒ€ν•œ ν™œμš©. +
  • +
+
+ +
+

πŸš€ μ£Όμš” κΈ°λŠ₯

    @@ -42,21 +59,21 @@ const Info = () => {

    πŸ‘€ μ‚¬μš©μž μ‹œλ‚˜λ¦¬μ˜€

    • - βœ… μ‚¬μš©μžλŠ” 졜고 κ΄€λ¦¬μžλ‘œ, νŒ€μ›μ—κ²Œ 관리 μ„œλΉ„μŠ€λ₯Ό μ΄μš©ν•  수 μžˆλŠ” + πŸ”Ή μ‚¬μš©μžλŠ” 졜고 κ΄€λ¦¬μžλ‘œ, νŒ€μ›μ—κ²Œ 관리 μ„œλΉ„μŠ€λ₯Ό μ΄μš©ν•  수 μžˆλŠ” 계정을 λ§Œλ“€μ–΄ μ œκ³΅ν•  수 μžˆλ‹€.
    • -
    • βœ… μ‚¬μš©μžλŠ” κ΄€λ¦¬μžμ˜ κΆŒν•œμ„ μˆ˜μ •ν•  수 μžˆλ‹€.
    • +
    • πŸ”Ή μ‚¬μš©μžλŠ” κ΄€λ¦¬μžμ˜ κΆŒν•œμ„ μˆ˜μ •ν•  수 μžˆλ‹€.
    • - βœ… μ‚¬μš©μžλŠ” κ΄€λ¦¬μžλ₯Ό μ‚­μ œν•  수 μžˆλ‹€.(일반 κ΄€λ¦¬μžλ§Œ μ‚­μ œ κ°€λŠ₯) + πŸ”Ή μ‚¬μš©μžλŠ” κ΄€λ¦¬μžλ₯Ό μ‚­μ œν•  수 μžˆλ‹€.(일반 κ΄€λ¦¬μžλ§Œ μ‚­μ œ κ°€λŠ₯)
    • -
    • βœ… μ‚¬μš©μžλŠ” κ΄€λ¦¬μž λͺ©λ‘μ„ 좜λ ₯ν•  수 μžˆλ‹€.
    • -
    • βœ… μ‚¬μš©μžλŠ” ν΄λΌμ΄μ–ΈνŠΈμ˜ 상세정보λ₯Ό λ³Ό 수 μžˆλ‹€.
    • -
    • βœ… μ‚¬μš©μžλŠ” ν΄λΌμ΄μ–ΈνŠΈμ˜ 정보λ₯Ό μˆ˜μ •ν•  수 μžˆλ‹€.
    • -
    • βœ… μ‚¬μš©μžλŠ” ν΄λΌμ΄μ–ΈνŠΈλ₯Ό μΆ”κ°€ν•  수 μžˆλ‹€.
    • -
    • βœ… μ‚¬μš©μžλŠ” ν΄λΌμ΄μ–ΈνŠΈλ₯Ό μ‚­μ œν•  수 μžˆλ‹€.
    • -
    • βœ… μ‚¬μš©μžλŠ” 지도 κΈ°λŠ₯을 μ΄μš©ν•  수 μžˆλ‹€.
    • -
    • βœ… μ‚¬μš©μžλŠ” 지역별 ν΄λΌμ΄μ–ΈνŠΈμ˜ 밀집도λ₯Ό 확인할 수 μžˆλ‹€.
    • -
    • βœ… μ‚¬μš©μžλŠ” 지역별 ν΄λΌμ΄μ–ΈνŠΈμ˜ 정보λ₯Ό λ³Ό 수 μžˆλ‹€.
    • +
    • πŸ”Ή μ‚¬μš©μžλŠ” κ΄€λ¦¬μž λͺ©λ‘μ„ 좜λ ₯ν•  수 μžˆλ‹€.
    • +
    • πŸ”Ή μ‚¬μš©μžλŠ” ν΄λΌμ΄μ–ΈνŠΈμ˜ 상세정보λ₯Ό λ³Ό 수 μžˆλ‹€.
    • +
    • πŸ”Ή μ‚¬μš©μžλŠ” ν΄λΌμ΄μ–ΈνŠΈμ˜ 정보λ₯Ό μˆ˜μ •ν•  수 μžˆλ‹€.
    • +
    • πŸ”Ή μ‚¬μš©μžλŠ” ν΄λΌμ΄μ–ΈνŠΈλ₯Ό μΆ”κ°€ν•  수 μžˆλ‹€.
    • +
    • πŸ”Ή μ‚¬μš©μžλŠ” ν΄λΌμ΄μ–ΈνŠΈλ₯Ό μ‚­μ œν•  수 μžˆλ‹€.
    • +
    • πŸ”Ή μ‚¬μš©μžλŠ” 지도 κΈ°λŠ₯을 μ΄μš©ν•  수 μžˆλ‹€.
    • +
    • πŸ”Ή μ‚¬μš©μžλŠ” 지역별 ν΄λΌμ΄μ–ΈνŠΈμ˜ 밀집도λ₯Ό 확인할 수 μžˆλ‹€.
    • +
    • πŸ”Ή μ‚¬μš©μžλŠ” 지역별 ν΄λΌμ΄μ–ΈνŠΈμ˜ 정보λ₯Ό λ³Ό 수 μžˆλ‹€.
From 304eb9bc96779be1b01984909f4fd0c5f015f56c Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:55:29 +0900 Subject: [PATCH 04/18] =?UTF-8?q?chore:=20API=5FBASE=5FURL=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/KakaoMap.js | 82 ++++++++++++++++++++++---------------- 1 file changed, 48 insertions(+), 34 deletions(-) diff --git a/src/components/KakaoMap.js b/src/components/KakaoMap.js index caa413e..927118d 100644 --- a/src/components/KakaoMap.js +++ b/src/components/KakaoMap.js @@ -2,47 +2,65 @@ import React, { useState, useEffect } from "react"; import { Map, MapMarker } from "react-kakao-maps-sdk"; import "../styles/KakaoMap.css"; +const API_BASE_URL = process.env.REACT_APP_API_URL; +const JS_KEY = process.env.REACT_APP_KAKAO_JS_KEY; + const KakaoMap = ({ lat, lng, width = "100%", height = "400px" }) => { - const [position, setPosition] = useState(null); - const [mapInstance, setMapInstance] = useState(null); + const [position, setPosition] = useState({ lat: 37.5665, lng: 126.978 }); // κΈ°λ³Έ μ’Œν‘œ μ„€μ • useEffect(() => { - console.log(`πŸ“Œ KakaoMap μ»΄ν¬λ„ŒνŠΈ - lat: ${lat}, lng: ${lng}`); - - if (!lat || !lng) { - console.warn("⚠️ 지도에 ν‘œμ‹œν•  μœ„μΉ˜ 정보가 μ—†μŠ΅λ‹ˆλ‹€."); - setPosition(null); - return; + console.log("πŸ“Œ KakaoMap μœ„μΉ˜ μ—…λ°μ΄νŠΈ - lat:", lat, "lng:", lng); + if (lat && lng) { + setPosition({ lat, lng }); } - - const newPosition = { lat: parseFloat(lat), lng: parseFloat(lng) }; - console.log(`βœ… μœ„μΉ˜ κ°’ μ„€μ •:`, newPosition); - setPosition(newPosition); }, [lat, lng]); useEffect(() => { - if (mapInstance && position) { - console.log("πŸ”„ 지도 재배치 (relayout) 싀행됨!"); + console.log("πŸ“ KakaoMap useEffect 싀행됨 - position λ³€κ²½ 감지"); + console.log(`πŸ“Œ ν˜„μž¬ position μƒνƒœ:`, position); + }, [position]); + + useEffect(() => { + console.log("πŸ“‘ λ°±μ—”λ“œμ—μ„œ μœ„μΉ˜ 정보 κ°€μ Έμ˜€λŠ” 쀑..."); + const fetchClientLocation = async () => { + try { + const response = await fetch(`${API_BASE_URL}/api/users/location`); + if (!response.ok) throw new Error("μœ„μΉ˜ 정보λ₯Ό λΆˆλŸ¬μ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."); + + const locationData = await response.json(); + console.log("πŸ“Œ κ°€μ Έμ˜¨ μœ„μΉ˜ 데이터:", locationData); - setTimeout(() => { - try { - if (mapInstance && typeof mapInstance.relayout === "function") { - mapInstance.relayout(); - mapInstance.setCenter(position); - } else { - console.warn("⚠️ mapInstanceκ°€ 아직 μƒμ„±λ˜μ§€ μ•ŠμŒ."); - } - } catch (error) { - console.error("🚨 Kakao 지도 κ΄€λ ¨ 였λ₯˜ λ°œμƒ:", error); - } - }, 500); + setPosition({ + lat: locationData.x || 37.5665, + lng: locationData.y || 126.978, + }); + } catch (error) { + console.error("🚨 μœ„μΉ˜ 정보 κ°€μ Έμ˜€κΈ° 였λ₯˜:", error); + } + }; + + if (!lat || !lng) { + fetchClientLocation(); } - }, [mapInstance, position]); + }, [lat, lng]); useEffect(() => { - console.log("πŸ“ KakaoMap useEffect 싀행됨"); - console.log(`πŸ“Œ ν˜„μž¬ position μƒνƒœ:`, position); - }, [position]); + if (!JS_KEY) { + console.error("🚨 Kakao JavaScript API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€!"); + return; + } + + if (document.getElementById("kakao-map-script")) return; + + const script = document.createElement("script"); + script.id = "kakao-map-script"; + script.src = `https://dapi.kakao.com/v2/maps/sdk.js?appkey=${JS_KEY}&libraries=services`; + script.async = true; + script.onload = () => { + console.log("βœ… Kakao 지도 API λ‘œλ“œ μ™„λ£Œ!"); + }; + document.head.appendChild(script); + }, []); return (
{ center={position} level={2} style={{ width: "70%", height: "100%", minHeight: "400px" }} - onCreate={(map) => { - console.log("πŸ—ΊοΈ Kakao Map 객체 생성됨:", map); - setMapInstance(map); - }} > From 10e0fdda0ceb6eb842550a4bcf190d4457b7cb4f Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:55:35 +0900 Subject: [PATCH 05/18] =?UTF-8?q?chore:=20API=5FBASE=5FURL=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Login.js | 47 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/src/pages/Login.js b/src/pages/Login.js index 7e44cff..1356f52 100644 --- a/src/pages/Login.js +++ b/src/pages/Login.js @@ -2,18 +2,54 @@ import React, { useState } from "react"; import { useNavigate } from "react-router-dom"; import "../styles/Login.css"; +const API_BASE_URL = process.env.REACT_APP_API_URL; + const Login = () => { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); + const [error, setError] = useState(""); const navigate = useNavigate(); const handleLogin = async (e) => { e.preventDefault(); + setError(""); + + try { + const response = await fetch(`${API_BASE_URL}/api/admin/login`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ email, password }), + credentials: "include", + }); + + console.log("πŸ“Œ 헀더:", [...response.headers.entries()]); + console.log("πŸ“Œ μƒνƒœ μ½”λ“œ:", response.status); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + errorData.message || "둜그인 μ‹€νŒ¨! 이메일 λ˜λŠ” λΉ„λ°€λ²ˆν˜Έλ₯Ό ν™•μΈν•˜μ„Έμš”." + ); + } + + const data = await response.json(); + console.log("πŸ“Œ 둜그인 응닡 데이터:", data); - localStorage.setItem("token", "dummy_token"); // 더미 둜그인 처리 - window.dispatchEvent(new Event("storage")); // μƒνƒœ λ³€κ²½ μ•Œλ¦Ό - alert("둜그인 성곡!"); - navigate("/"); + if (!data.Accesstoken) { + throw new Error("토큰이 응닡에 μ—†μŠ΅λ‹ˆλ‹€."); + } + + localStorage.setItem("token", data.Accesstoken); + window.dispatchEvent(new Event("storage")); + + alert("둜그인 성곡!"); + navigate("/"); + } catch (err) { + console.error("🚨 둜그인 였λ₯˜:", err); + setError(err.message); + } }; return ( @@ -22,6 +58,9 @@ const Login = () => {

μ‚¬μš©μž 관리 μ„œλΉ„μŠ€μ— μ˜€μ‹  것을 ν™˜μ˜ν•©λ‹ˆλ‹€!

쉽고, λΉ λ₯΄κ²Œ μ—°κ²°λ˜λŠ” 카카였의 λ‹€μ–‘ν•œ μ„œλΉ„μŠ€λ₯Ό κ²½ν—˜ν•΄λ³΄μ„Έμš”.

+ + {error &&

{error}

} +
Date: Tue, 18 Feb 2025 10:55:44 +0900 Subject: [PATCH 06/18] =?UTF-8?q?chore:=20API=5FBASE=5FURL=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/MapFilterControls.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MapFilterControls.js b/src/components/MapFilterControls.js index 48bce16..f50114b 100644 --- a/src/components/MapFilterControls.js +++ b/src/components/MapFilterControls.js @@ -1,7 +1,7 @@ import React, { useState, useEffect } from "react"; import "../styles/MapFilterControls.css"; -const KAKAO_API_KEY = process.env.REACT_APP_KAKAO_API_KEY; +const KAKAO_API_KEY = process.env.REACT_APP_KAKAO_JS_KE; const MapFilterControls = ({ customers, setFilteredCustomers }) => { const [map, setMap] = useState(null); From 5dacd1c435353f52856c4def88d87bad89741d24 Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:56:06 +0900 Subject: [PATCH 07/18] =?UTF-8?q?chore:=20=EA=B4=80=EB=A6=AC=EC=9E=90=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=ED=8E=98=EC=9D=B4=EC=A7=80=20css=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/AdminManagement.css | 60 ++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/styles/AdminManagement.css b/src/styles/AdminManagement.css index ede1f2d..14723ab 100644 --- a/src/styles/AdminManagement.css +++ b/src/styles/AdminManagement.css @@ -33,6 +33,7 @@ cursor: pointer; border-radius: 6px; margin-left: 12px; + width: 170px; } .search-container .add-btn:hover { @@ -68,6 +69,27 @@ background: #1a4cd8; } +.search-btn { + padding: 10px; + font-size: 16px; + font-weight: 900; + width: 100px; + height: 50px; + border: 1px solid #ddd; + border-radius: 6px; + background: #2b5ef7; + color: white; + cursor: pointer; +} + +.search-btn:hover { + background: #1a4cd8 !important; +} + +.edit-modal-overlay .add-admin-save-btn { + margin-top: 15px; +} + .admin-list { text-align: left; } @@ -116,6 +138,7 @@ } .edit-modal { + position: relative; background: white; padding: 20px; width: 350px; @@ -147,6 +170,10 @@ border-radius: 6px; } +.no-access { + margin-top: 30%; +} + .no-access h2 { text-align: center; font-size: 22px; @@ -159,3 +186,36 @@ margin: 40px auto; max-width: 500px; } + +.close-modal-btn { + position: absolute; + top: 10px; + left: 350px; + background: none; + border: none; + font-size: 25px; + cursor: pointer; + color: #333; + font-weight: bold; + z-index: 1000; +} + +.close-modal-btn:hover { + color: red; +} + +.modal-buttons .delete-confirm-btn { + background-color: red; +} + +.modal-buttons .delete-confirm-btn:hover { + background-color: darkred; +} + +.modal-buttons .cancel-btn { + background-color: #949393; +} + +.modal-buttons .cancel-btn:hover { + background-color: #6a6a6a; +} From 90a27095e34e2bd1fa20005fbe2d41187081676b Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:56:13 +0900 Subject: [PATCH 08/18] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EA=B4=80=EB=A6=AC=20=EA=B4=80=EB=A0=A8=20=ED=8E=98=EC=9D=B4?= =?UTF-8?q?=EC=A7=80=20css=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/ClientManagement.css | 108 ++++++++++++++++++++++++++++---- 1 file changed, 97 insertions(+), 11 deletions(-) diff --git a/src/styles/ClientManagement.css b/src/styles/ClientManagement.css index 08bd478..dc223d9 100644 --- a/src/styles/ClientManagement.css +++ b/src/styles/ClientManagement.css @@ -9,6 +9,11 @@ display: flex; flex-direction: column; gap: 15px; + margin-top: 10px; +} + +.client-management h2 { + height: 5px; } .search-container { @@ -17,8 +22,9 @@ justify-content: space-between; align-items: center; flex-wrap: wrap; - margin-bottom: 15px; + margin-bottom: 35px; width: 100%; + margin-top: -6px; } .search-container select { @@ -37,6 +43,23 @@ font-size: 16px; } +.search-container .reset-btn { + background: #2b5ef7; + color: white; + padding: 10px; + font-size: 16px; + cursor: pointer; + border: none; + border-radius: 6px; + width: 80px; + height: 50px; + font-weight: bolder; +} + +.search-container .reset-btn:hover { + background: #1a4cd8; +} + .search-container .add-btn { background: #2b5ef7; color: white; @@ -45,6 +68,8 @@ border: none; cursor: pointer; border-radius: 6px; + width: 100px !important; + height: 50px; } .search-container .add-btn:hover { @@ -82,7 +107,7 @@ .close-modal-btn { position: absolute; - top: 10px; + top: 20px; right: 15px; background: none !important; border: none; @@ -93,6 +118,7 @@ .client-container { width: 900px; + margin-top: -30px; } .close-modal-btn:hover { @@ -103,8 +129,8 @@ display: flex; flex-direction: column; gap: 10px; - flex-grow: 1; /* πŸ”₯ λ‚΄λΆ€ λ‚΄μš©μ΄ λŠ˜μ–΄λ‚˜λ„ 슀크둀 적용 */ - padding-bottom: 40px; /* πŸ”₯ ν‘Έν„° 높이λ₯Ό κ³ λ €ν•œ μΆ”κ°€ λ²„νŠΌ μ—¬λ°± */ + flex-grow: 1; + padding-bottom: 40px; } .client-form label { @@ -120,7 +146,7 @@ } .client-form select { - width: 106%; /* μž…λ ₯μ°½κ³Ό λ™μΌν•œ λ„ˆλΉ„ μ„€μ • */ + width: 106%; padding: 10px; font-size: 16px; border: 1px solid #ccc; @@ -327,6 +353,10 @@ margin-right: 10px; } +.save-btn:hover { + background-color: #1a4cd8; +} + .cancel-btn { background: #ccc; color: black; @@ -371,7 +401,8 @@ .history-modal { background: white; padding: 20px; - width: 400px; + width: 500px; + height: 60%; border-radius: 8px; box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.2); text-align: center; @@ -383,11 +414,28 @@ overflow-y: auto; } +.history-modal h3 { + margin-left: 37%; + font-size: 22px; +} + +.history-modal-header { + position: sticky; + top: 0; + background: white; + z-index: 10; + padding: 10px 20px; + display: flex; + justify-content: space-between; + align-items: center; + border-bottom: 1px solid #ddd; +} + .history-modal ul { list-style: none; padding: 0; max-height: 200px; - overflow-y: auto; + overflow-y: 50%; text-align: left; } @@ -401,8 +449,8 @@ } .history-modal-overlay .close-modal-btn { - right: 10px; - top: -10px; + margin-left: 23%; + top: 10px; font-size: 20px; } @@ -428,8 +476,8 @@ border: 1px solid #ddd; list-style-type: none; padding: 5px; - max-height: 150px; /* μ΅œλŒ€ 높이 μ„€μ • */ - overflow-y: auto; /* μ„Έλ‘œ 슀크둀 μΆ”κ°€ */ + max-height: 150px; + overflow-y: auto; width: calc(100% - 20px); z-index: 1000; } @@ -442,3 +490,41 @@ .address-suggestions li:hover { background-color: #f1f1f1; } + +.pagination { + display: flex; + justify-content: center; + align-items: center; + margin-top: -10px; + gap: 8px; +} + +.pagination button { + background: none; + border: 1px solid #ddd; + color: black; + font-size: 16px; + cursor: pointer; + padding: 8px 12px; + border-radius: 4px; + transition: all 0.2s ease-in-out; +} + +.pagination button:hover { + background: #f0f0f0; +} + +.pagination button.active { + font-weight: bold; + color: white; + background: #2b5ef7; + font-size: 18px; + border-radius: 6px; + padding: 10px 14px; + height: auto; +} + +.pagination button:disabled { + color: #aaa; + cursor: not-allowed; +} From 1242a4439b41e233c40951d644dba6b0405d908c Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:56:46 +0900 Subject: [PATCH 09/18] =?UTF-8?q?chore:=20axios=20=EB=B2=84=EC=A0=BC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20=EB=B0=8F=20jwt=20=EB=94=94=EC=BD=94?= =?UTF-8?q?=EB=94=A9=20=ED=8E=98=ED=82=A4=EC=A7=80=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package-lock.json | 39 +++++++++++++++++++++++++++++++++++++++ package.json | 2 ++ 2 files changed, 41 insertions(+) diff --git a/package-lock.json b/package-lock.json index a1e3d82..067f9d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "frontend", "version": "0.1.0", "dependencies": { + "axios": "^1.7.9", "cra-template": "1.2.0", + "jwt-decode": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-kakao-maps-sdk": "^1.1.27", @@ -4866,6 +4868,30 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -11121,6 +11147,14 @@ "node": ">=4.0" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "engines": { + "node": ">=18" + } + }, "node_modules/kakao.maps.d.ts": { "version": "0.1.40", "resolved": "https://registry.npmjs.org/kakao.maps.d.ts/-/kakao.maps.d.ts-0.1.40.tgz", @@ -13719,6 +13753,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", diff --git a/package.json b/package.json index e989c44..69a931a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "version": "0.1.0", "private": true, "dependencies": { + "axios": "^1.7.9", "cra-template": "1.2.0", + "jwt-decode": "^4.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-kakao-maps-sdk": "^1.1.27", From 18f4dd41fd42bd62835dd86aa8c26034d313554a Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:57:29 +0900 Subject: [PATCH 10/18] =?UTF-8?q?faet:=20jwt=EC=A0=95=EB=B3=B4=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C=ED=99=94=20=EB=B0=8F=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/MyPage.js | 120 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 93 insertions(+), 27 deletions(-) diff --git a/src/pages/MyPage.js b/src/pages/MyPage.js index 5b268e4..8804f25 100644 --- a/src/pages/MyPage.js +++ b/src/pages/MyPage.js @@ -1,10 +1,15 @@ import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { jwtDecode } from "jwt-decode"; import "../styles/MyPage.css"; +const API_BASE_URL = `${process.env.REACT_APP_API_URL}/api/admin`; + const MyPage = () => { const [userInfo, setUserInfo] = useState({ - email: "admin@example.com", - role: "SUPER", // λ˜λŠ” "NORMAL" + email: "", + role: "", + id: null, }); const [isModalOpen, setIsModalOpen] = useState(false); @@ -14,12 +19,52 @@ const MyPage = () => { const [message, setMessage] = useState(""); useEffect(() => { - const storedUser = JSON.parse(localStorage.getItem("user")); - if (storedUser) { - setUserInfo(storedUser); - } + loadAdminInfoFromToken(); }, []); + const loadAdminInfoFromToken = () => { + let token = localStorage.getItem("token")?.trim(); + if (!token || !token.startsWith("Bearer ")) { + alert("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€."); + return; + } + + token = token.replace("Bearer ", ""); + + try { + const decodedToken = jwtDecode(token); + console.log("πŸ”‘ λ””μ½”λ”©λœ JWT 정보:", decodedToken); + + const currentTimestamp = Math.floor(Date.now() / 1000); + if (decodedToken.exp < currentTimestamp) { + console.error("❌ JWT 토큰 만료됨!"); + alert("μ„Έμ…˜μ΄ λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ λ‘œκ·ΈμΈν•΄μ£Όμ„Έμš”."); + localStorage.removeItem("token"); + window.location.href = "/login"; + return; + } + + if (!decodedToken.AdminId) { + console.error("❌ JWTμ—μ„œ IDκ°€ μ—†μŒ!", decodedToken); + alert("둜그인 정보λ₯Ό 확인할 수 μ—†μŠ΅λ‹ˆλ‹€. λ‹€μ‹œ λ‘œκ·ΈμΈν•΄μ£Όμ„Έμš”."); + localStorage.removeItem("token"); + window.location.href = "/login"; + return; + } + + setUserInfo({ + email: decodedToken.email || "이메일 정보 μ—†μŒ", + role: decodedToken.role || "κΆŒν•œ 정보 μ—†μŒ", + id: decodedToken.AdminId, + }); + } catch (error) { + console.error("❌ 토큰 λ””μ½”λ”© 쀑 였λ₯˜ λ°œμƒ:", error); + alert("μ„Έμ…˜μ΄ λ§Œλ£Œλ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ λ‘œκ·ΈμΈν•΄μ£Όμ„Έμš”."); + localStorage.removeItem("token"); + window.location.href = "/login"; + } + }; + const handleCloseModal = () => { setIsModalOpen(false); setMessage(""); @@ -28,7 +73,7 @@ const MyPage = () => { setConfirmPassword(""); }; - const handlePasswordChange = (e) => { + const handlePasswordChange = async (e) => { e.preventDefault(); if (!currentPassword || !newPassword || !confirmPassword) { @@ -37,23 +82,54 @@ const MyPage = () => { } if (newPassword !== confirmPassword) { - setMessage("μƒˆλ‘œμš΄ λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + setMessage("μƒˆ λΉ„λ°€λ²ˆν˜Έκ°€ μΌμΉ˜ν•˜μ§€ μ•ŠμŠ΅λ‹ˆλ‹€."); + return; + } + + if (!userInfo.id) { + console.error("❌ κ΄€λ¦¬μž IDκ°€ μ—†μŒ!"); + alert("κ΄€λ¦¬μž 정보λ₯Ό 뢈러올 수 μ—†μŠ΅λ‹ˆλ‹€. λ‹€μ‹œ λ‘œκ·ΈμΈν•΄μ£Όμ„Έμš”."); + return; + } + + let token = localStorage.getItem("token"); + if (!token) { + alert("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€."); return; } - setTimeout(() => { - setMessage("λΉ„λ°€λ²ˆν˜Έκ°€ μ„±κ³΅μ μœΌλ‘œ λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€."); + token = token.replace("Bearer ", "").trim(); + + console.log("πŸ”‘ ν˜„μž¬ 토큰:", token); + + try { + console.log("πŸ”„ λΉ„λ°€λ²ˆν˜Έ λ³€κ²½ μš”μ²­ 쀑...", userInfo.id); + await axios.patch( + `${API_BASE_URL}/${userInfo.id}/password`, + { + oldPassword: currentPassword, + newPassword: newPassword, + }, + { + headers: { Authorization: `Bearer ${token}` }, + } + ); + + setMessage("βœ… λΉ„λ°€λ²ˆν˜Έκ°€ μ„±κ³΅μ μœΌλ‘œ λ³€κ²½λ˜μ—ˆμŠ΅λ‹ˆλ‹€."); setTimeout(handleCloseModal, 1000); - }, 1000); + } catch (error) { + console.error("❌ λΉ„λ°€λ²ˆν˜Έ λ³€κ²½ 였λ₯˜:", error); + setMessage( + error.response?.data?.message || "λΉ„λ°€λ²ˆν˜Έ 변경에 μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€." + ); + } }; return ( <>
-

κ΄€λ¦¬μž 정보

-

이메일: {userInfo.email} @@ -63,7 +139,6 @@ const MyPage = () => { {userInfo.role === "SUPER" ? "졜고 κ΄€λ¦¬μž" : "일반 κ΄€λ¦¬μž"}

- {message &&

{message}

} -
From 98fd2df32d96fe4d6eba75e82a7e4486d7d9cdeb Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:57:40 +0900 Subject: [PATCH 11/18] =?UTF-8?q?chore:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20css=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/MainPage.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/MainPage.css b/src/styles/MainPage.css index 82caed9..85c05de 100644 --- a/src/styles/MainPage.css +++ b/src/styles/MainPage.css @@ -54,4 +54,5 @@ button:hover { width: 100%; height: 85%; object-fit: cover; + margin-top: 2%; } From 3fe4d8a94eb9da70f47075d3761c8132ce456c11 Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:57:45 +0900 Subject: [PATCH 12/18] =?UTF-8?q?chore:=20=EB=A7=88=EC=9D=B4=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20css=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/MyPage.css | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/styles/MyPage.css b/src/styles/MyPage.css index 95fe6c0..0a34c82 100644 --- a/src/styles/MyPage.css +++ b/src/styles/MyPage.css @@ -96,7 +96,8 @@ h2 { } .modal-content button { - margin-top: -12px; + margin-top: 5px; + width: 340px; padding: 10px; font-size: 16px; background-color: black; @@ -111,7 +112,8 @@ h2 { } .close-modal-btn { - margin-top: 10px; + margin-top: -10px !important; + margin-right: -155px; background: red; color: white; padding: 8px; From f5c6997eff31116f1aebf534b80e5f3f64a87fc2 Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:57:50 +0900 Subject: [PATCH 13/18] =?UTF-8?q?chore:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20css=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/Login.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/styles/Login.css b/src/styles/Login.css index 7b5407d..9de9dd5 100644 --- a/src/styles/Login.css +++ b/src/styles/Login.css @@ -147,3 +147,9 @@ input { height: auto; } } + +.error-message { + color: red; + font-size: 14px; + margin-bottom: 10px; +} From fa4d8a13461e885cc8a45f38cc612b4ce6204d02 Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:57:54 +0900 Subject: [PATCH 14/18] =?UTF-8?q?chore:=20=EC=86=8C=EA=B0=9C=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20css=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/styles/Info.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/styles/Info.css b/src/styles/Info.css index 11e67d5..6b21bac 100644 --- a/src/styles/Info.css +++ b/src/styles/Info.css @@ -33,6 +33,7 @@ backdrop-filter: blur(8px); color: #000000; overflow-y: auto; + margin-top: 190px; } .info-container h1 { From 26ca58c680a3a8a9cf28f8ab5034c9dff4cb6ced Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:58:05 +0900 Subject: [PATCH 15/18] =?UTF-8?q?chore:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B8=B0=EB=8A=A5=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/Header.js | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/components/Header.js b/src/components/Header.js index 9e72074..4c3e02d 100644 --- a/src/components/Header.js +++ b/src/components/Header.js @@ -1,7 +1,10 @@ import React, { useState, useEffect } from "react"; import { Link, useNavigate } from "react-router-dom"; +import axios from "axios"; import "../styles/Header.css"; +const API_BASE_URL = process.env.REACT_APP_API_URL; + const Header = () => { const [isAuthenticated, setIsAuthenticated] = useState( !!localStorage.getItem("token") @@ -17,12 +20,29 @@ const Header = () => { return () => window.removeEventListener("storage", handleStorageChange); }, []); - const handleLogout = () => { + const handleLogout = async () => { const confirmLogout = window.confirm("λ‘œκ·Έμ•„μ›ƒ ν•˜μ‹œκ² μŠ΅λ‹ˆκΉŒ?"); - if (confirmLogout) { + if (!confirmLogout) return; + + try { + const token = localStorage.getItem("token"); + if (!token) throw new Error("토큰이 μ—†μŠ΅λ‹ˆλ‹€."); + + await axios.post( + `${API_BASE_URL}/api/admin/logout`, + {}, + { + headers: { Authorization: `Bearer ${token}` }, + withCredentials: true, + } + ); + localStorage.removeItem("token"); setIsAuthenticated(false); navigate("/"); + } catch (error) { + console.error("❌ λ‘œκ·Έμ•„μ›ƒ μ‹€νŒ¨:", error); + alert("λ‘œκ·Έμ•„μ›ƒμ— μ‹€νŒ¨ν–ˆμŠ΅λ‹ˆλ‹€."); } }; From cd7fb03e844a2626c75e74447de4225ae9a6f015 Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:58:37 +0900 Subject: [PATCH 16/18] =?UTF-8?q?chore:=20=EC=A7=80=EC=97=AD=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A7=81=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EB=B2=94?= =?UTF-8?q?=EB=A1=80=20=ED=95=9C=EA=B8=80=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/HeatMap.js | 337 +++++++++++++++++++++++++++++--------- 1 file changed, 258 insertions(+), 79 deletions(-) diff --git a/src/components/HeatMap.js b/src/components/HeatMap.js index f8677f1..4999fb7 100644 --- a/src/components/HeatMap.js +++ b/src/components/HeatMap.js @@ -1,19 +1,28 @@ import React, { useEffect, useState, useCallback } from "react"; import "../styles/HeatMap.css"; -import DummyData from "../data/DummyData"; const KAKAO_API_KEY = process.env.REACT_APP_KAKAO_API_KEY; +const API_BASE_URL = process.env.REACT_APP_API_URL; const HeatMap = () => { const [map, setMap] = useState(null); - const [customers] = useState(DummyData || []); - const [filteredCustomers, setFilteredCustomers] = useState(DummyData || []); + const [customers, setCustomers] = useState([]); + const [filteredCustomers, setFilteredCustomers] = useState([]); const [selectedAges, setSelectedAges] = useState([]); - const [customMinAge, setCustomMinAge] = useState(""); - const [customMaxAge, setCustomMaxAge] = useState(""); const [selectedRegions, setSelectedRegions] = useState([]); const [searchTerm, setSearchTerm] = useState(""); const [clusterer, setClusterer] = useState(null); + const [tempSelectedAges, setTempSelectedAges] = useState([]); + const [tempSelectedRegions, setTempSelectedRegions] = useState([]); + + const ageGroups = { + TEENS: "10λŒ€", + TWENTIES: "20λŒ€", + THIRTIES: "30λŒ€", + FORTIES: "40λŒ€", + FIFTIES: "50λŒ€", + SIXTIES: "60λŒ€ 이상", + }; const initializeMap = useCallback(() => { if (!window.kakao || !window.kakao.maps || map) return; @@ -54,19 +63,79 @@ const HeatMap = () => { } }, [initializeMap]); + const fetchHeatMapData = useCallback(async () => { + try { + let token = localStorage.getItem("token"); + if (!token || token.trim() === "") { + throw new Error("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€."); + } + + token = token.trim(); + let formattedToken = token.startsWith("Bearer ") + ? token + : `Bearer ${token}`; + console.log("πŸ“Œ JWT 토큰 확인:", formattedToken); + + let queryParams = new URLSearchParams(); + + if (selectedRegions.length > 0) { + selectedRegions.forEach((region) => + queryParams.append("region", region) + ); + } + + if (selectedAges.length > 0) { + selectedAges.forEach((age) => { + const formattedAge = age === "SIXTIES" ? "SIXTIES_AND_ABOVE" : age; + queryParams.append("ageGroups", formattedAge); + }); + } + + const url = queryParams.toString() + ? `${API_BASE_URL}/api/users/heatmap?${queryParams.toString()}` + : `${API_BASE_URL}/api/users/heatmap`; + + console.log("πŸ“Œ API μš”μ²­ URL:", url); + + const response = await fetch(url, { + method: "GET", + headers: { + Authorization: formattedToken, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) throw new Error("히트맡 데이터 뢈러였기 μ‹€νŒ¨"); + + const data = await response.json(); + console.log("πŸ“Œ 히트맡 데이터:", data); + + setCustomers(data.response || []); + setFilteredCustomers(data.response || []); + } catch (error) { + console.error("🚨 히트맡 데이터 κ°€μ Έμ˜€κΈ° 였λ₯˜:", error); + } + }, [selectedRegions, selectedAges]); + + useEffect(() => { + fetchHeatMapData(); + }, [fetchHeatMapData]); + + useEffect(() => { + if (selectedRegions.length > 0 || selectedAges.length > 0) { + fetchHeatMapData(); + } + }, [selectedRegions, selectedAges, fetchHeatMapData]); + const applyClusterer = useCallback(() => { if (!map || !clusterer) return; clusterer.clear(); - const bounds = new window.kakao.maps.LatLngBounds(); const markers = filteredCustomers .map((customer) => { - if (!customer.lat || !customer.lng) return null; - const position = new window.kakao.maps.LatLng( - customer.lat, - customer.lng - ); + if (!customer.x || !customer.y) return null; + const position = new window.kakao.maps.LatLng(customer.y, customer.x); bounds.extend(position); return new window.kakao.maps.Marker({ position }); }) @@ -82,6 +151,18 @@ const HeatMap = () => { } }, [map, clusterer, filteredCustomers, applyClusterer]); + const handleApplyFilter = () => { + console.log("πŸ“Œ ν•„ν„° 적용 - μ„ νƒλœ μ—°λ ΉλŒ€:", tempSelectedAges); + console.log("πŸ“Œ ν•„ν„° 적용 - μ„ νƒλœ μ§€μ—­:", tempSelectedRegions); + + setSelectedAges(tempSelectedAges); + setSelectedRegions(tempSelectedRegions); + }; + + useEffect(() => { + fetchHeatMapData(); + }, [fetchHeatMapData]); + const handleSearch = () => { if (!searchTerm.trim()) { alert("검색어λ₯Ό μž…λ ₯ν•˜μ„Έμš”."); @@ -110,53 +191,168 @@ const HeatMap = () => { const handleAddRegion = () => { if (!searchTerm.trim()) { - alert("μΆ”κ°€ν•  지역을 κ²€μƒ‰ν•˜μ„Έμš”."); + alert("μΆ”κ°€ν•  지역을 μž…λ ₯ν•˜μ„Έμš”."); return; } - setSelectedRegions((prev) => [...prev, searchTerm]); - setSearchTerm(""); + const places = new window.kakao.maps.services.Places(); + const geocoder = new window.kakao.maps.services.Geocoder(); + + places.keywordSearch(searchTerm, (data, status) => { + if (status === window.kakao.maps.services.Status.OK && data.length > 0) { + const firstResult = data[0]; + console.log("πŸ“Œ κ²€μƒ‰λœ μ£Όμ†Œ 데이터:", firstResult); + + const { x, y } = firstResult; + + geocoder.coord2RegionCode(x, y, (result, status) => { + if ( + status === window.kakao.maps.services.Status.OK && + result.length > 0 + ) { + console.log("πŸ“Œ μ’Œν‘œ 기반 행정ꡬ역 데이터:", result); + + let selectedRegion = null; + + for (let region of result) { + if (region.region_type === "H") continue; + + if (region.region_1depth_name.includes(searchTerm)) { + selectedRegion = region.region_1depth_name; + } else if (region.region_2depth_name.includes(searchTerm)) { + selectedRegion = region.region_2depth_name; + } else if (region.region_3depth_name.includes(searchTerm)) { + selectedRegion = region.region_3depth_name; + } + } + + if (!selectedRegion) { + alert("μ˜¬λ°”λ₯Έ 지역을 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); + return; + } + + console.log("πŸ“Œ μΆ”κ°€ν•  μ§€μ—­:", selectedRegion); + + setTempSelectedRegions((prev) => { + if (prev.includes(selectedRegion)) { + alert("이미 μΆ”κ°€λœ μ§€μ—­μž…λ‹ˆλ‹€."); + return prev; + } + return [...prev, selectedRegion]; + }); + + setSearchTerm(""); + } else { + alert("검색 κ²°κ³Όλ₯Ό 찾을 수 μ—†μŠ΅λ‹ˆλ‹€."); + } + }); + } else { + alert("검색 κ²°κ³Όκ°€ μ—†μŠ΅λ‹ˆλ‹€."); + } + }); }; + useEffect(() => { + console.log("ν˜„μž¬ μ„ νƒλœ μ§€μ—­:", selectedRegions); + }, [selectedRegions]); + + const convertCoordinatesToRegion = useCallback((customers) => { + const geocoder = new window.kakao.maps.services.Geocoder(); + return Promise.all( + customers.map((customer) => { + return new Promise((resolve) => { + if (!customer.x || !customer.y) { + resolve(null); + return; + } + + geocoder.coord2Address(customer.x, customer.y, (result, status) => { + if (status === window.kakao.maps.services.Status.OK) { + const address = result[0].address; + const region = address.region_1depth_name; + const subRegion = address.region_2depth_name; + resolve({ ...customer, region, subRegion }); + } else { + resolve(null); + } + }); + }); + }) + ); + }, []); + + const fetchCustomers = useCallback(async () => { + try { + const token = localStorage.getItem("token")?.trim(); + if (!token) throw new Error("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€."); + + const response = await fetch(`${API_BASE_URL}/api/customers`, { + method: "GET", + headers: { + Authorization: `Bearer ${token.trim()}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) throw new Error("데이터 뢈러였기 μ‹€νŒ¨"); + + const data = await response.json(); + console.log("πŸ“Œ 원본 고객 데이터:", data); + + const customersWithRegion = await convertCoordinatesToRegion(data); + + const sanitizedCustomers = customersWithRegion.map((customer) => ({ + ...customer, + region: customer.region || "Unknown", + subRegion: customer.subRegion || "Unknown", + })); + + setCustomers(sanitizedCustomers); + setFilteredCustomers(sanitizedCustomers); + } catch (error) { + console.error("🚨 고객 데이터 κ°€μ Έμ˜€κΈ° 였λ₯˜:", error); + } + }, [convertCoordinatesToRegion]); + + useEffect(() => { + fetchCustomers(); + }, [fetchCustomers]); + const handleResetFilters = () => { + console.log("πŸ“Œ ν•„ν„° μ΄ˆκΈ°ν™” μ‹€ν–‰"); + + setTempSelectedAges([]); + setTempSelectedRegions([]); setSelectedAges([]); - setCustomMinAge(""); - setCustomMaxAge(""); setSelectedRegions([]); - - if (clusterer) clusterer.clear(); - setFilteredCustomers([...DummyData]); }; - const handleFilter = () => { - let filtered = [...customers]; - - if (selectedAges.length > 0) { - filtered = filtered.filter((customer) => - selectedAges.some( - (age) => - customer.age >= age && customer.age < (age === 60 ? 150 : age + 10) - ) - ); + useEffect(() => { + if (selectedAges.length === 0 && selectedRegions.length === 0) { + fetchHeatMapData(); } + }, [selectedAges, selectedRegions, fetchHeatMapData]); - if (customMinAge && customMaxAge) { - filtered = filtered.filter( - (customer) => - customer.age >= Number(customMinAge) && - customer.age <= Number(customMaxAge) - ); - } + const handleFilter = useCallback(() => { + let filtered = [...customers]; if (selectedRegions.length > 0) { - filtered = filtered.filter((customer) => - selectedRegions.some((region) => customer.region.includes(region)) - ); + filtered = filtered.filter((customer) => { + const region = customer.region || ""; + const subRegion = customer.subRegion || ""; + return selectedRegions.some( + (regionFilter) => + region.includes(regionFilter) || subRegion.includes(regionFilter) + ); + }); } - if (clusterer) clusterer.clear(); setFilteredCustomers(filtered); - }; + }, [customers, selectedRegions]); + + useEffect(() => { + handleFilter(); + }, [handleFilter]); return (
@@ -167,39 +363,22 @@ const HeatMap = () => {

πŸ‘€ μ—°λ ΉλŒ€ 선택

- {[10, 20, 30, 40, 50, 60].map((age) => ( + {Object.keys(ageGroups).map((key) => ( ))}
- -

λ²”μœ„ μž…λ ₯

-
- setCustomMinAge(e.target.value)} - /> - ~ - setCustomMaxAge(e.target.value)} - /> -
@@ -214,23 +393,23 @@ const HeatMap = () => { /> -
-
- {selectedRegions.map((region, index) => ( - - {region} - - ))} +
+ {tempSelectedRegions.map((region, index) => ( + + {region} + + ))} +
-
-
- - +
+ + +
From 7ec99a98e791fba0660f804ccee88aca581ceff6 Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:59:01 +0900 Subject: [PATCH 17/18] =?UTF-8?q?chore:=20API=5FBASE=5FURL=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=ED=99=94=20=EB=B0=8F=20=EB=B2=94?= =?UTF-8?q?=EB=A1=80=20=ED=95=9C=EA=B8=80=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ClientRetention.js | 338 ++++++++++++++++++------------ 1 file changed, 209 insertions(+), 129 deletions(-) diff --git a/src/components/ClientRetention.js b/src/components/ClientRetention.js index 9d4e91f..1757f09 100644 --- a/src/components/ClientRetention.js +++ b/src/components/ClientRetention.js @@ -1,4 +1,5 @@ -import React, { useState } from "react"; +import React, { useState, useEffect, useCallback } from "react"; +import axios from "axios"; import { PieChart, Pie, @@ -7,9 +8,10 @@ import { Legend, ResponsiveContainer, } from "recharts"; -import DummyData from "../data/DummyData"; import "../styles/ClientRetention.css"; +const API_BASE_URL = `${process.env.REACT_APP_API_URL}/api/users/stats`; + const COLORS = [ "#8884d8", "#82ca9d", @@ -23,11 +25,6 @@ const COLORS = [ "#8E44AD", "#16A085", "#D35400", - "#C0392B", - "#1ABC9C", - "#F1C40F", - "#27AE60", - "#2C3E50", ]; const regions = [ @@ -50,62 +47,134 @@ const regions = [ "제주", ]; -const ageGroups = ["10λŒ€", "20λŒ€", "30λŒ€", "40λŒ€", "50λŒ€", "60λŒ€ 이상"]; +const ageGroups = { + TEENS: "10λŒ€", + TWENTIES: "20λŒ€", + THIRTIES: "30λŒ€", + FORTIES: "40λŒ€", + FIFTIES: "50λŒ€", + SIXTIES_AND_ABOVE: "60λŒ€ 이상", +}; + const genders = ["λ‚¨μž", "μ—¬μž"]; +const genderMapping = { + λ‚¨μž: ["MALE", "λ‚¨μž"], + μ—¬μž: ["FEMALE", "μ—¬μž"], +}; + const ClientRetention = () => { const [filterGender, setFilterGender] = useState(["전체"]); const [filterRegion, setFilterRegion] = useState(["전체"]); const [filterAgeGroup, setFilterAgeGroup] = useState(["전체"]); + const [genderData, setGenderData] = useState([]); + const [regionData, setRegionData] = useState([]); + const [ageData, setAgeData] = useState([]); + const [totalClients, setTotalClients] = useState(0); + const [loading, setLoading] = useState(true); const [dropdownOpen, setDropdownOpen] = useState({ gender: false, region: false, age: false, }); - const filteredClients = DummyData.filter((client) => { - const matchGender = - filterGender.includes("전체") || filterGender.includes(client.gender); - const matchRegion = - filterRegion.includes("전체") || filterRegion.includes(client.region); - const matchAge = - filterAgeGroup.includes("전체") || - filterAgeGroup.some((ageLabel) => { - const index = ageGroups.indexOf(ageLabel); - if (index === -1) return false; - const minAge = index * 10 + 10; - const maxAge = index === 5 ? 100 : minAge + 9; - return client.age >= minAge && client.age <= maxAge; + const fetchUserStats = useCallback(async () => { + setLoading(true); + try { + const rawToken = localStorage.getItem("token")?.trim(); + const token = rawToken?.startsWith("Bearer ") + ? rawToken + : `Bearer ${rawToken}`; + + const headers = { + Authorization: token, + "Content-Type": "application/json", + }; + + const params = new URLSearchParams(); + + if (!filterGender.includes("전체")) { + filterGender.forEach((g) => { + genderMapping[g].forEach((mappedValue) => { + params.append("gender", mappedValue); + }); + }); + } + + if (!filterRegion.includes("전체")) { + filterRegion.forEach((r) => params.append("region", r)); + } + + if (!filterAgeGroup.includes("전체")) { + filterAgeGroup.forEach((a) => + params.append( + "ageGroups", + Object.keys(ageGroups).find((key) => ageGroups[key] === a) + ) + ); + } + + const response = await axios.get(`${API_BASE_URL}?${params.toString()}`, { + withCredentials: true, + headers, }); - return matchGender && matchRegion && matchAge; - }); + const { total, genderStats, regionStats, ageStats } = response.data; + setTotalClients(total); + + const combinedGenderData = {}; + Object.entries(genderStats).forEach(([key, value]) => { + if (genderMapping["λ‚¨μž"].includes(key)) { + combinedGenderData["λ‚¨μž"] = + (combinedGenderData["λ‚¨μž"] || 0) + value; + } else if (genderMapping["μ—¬μž"].includes(key)) { + combinedGenderData["μ—¬μž"] = + (combinedGenderData["μ—¬μž"] || 0) + value; + } + }); + + setGenderData( + Object.entries(combinedGenderData) + .filter(([_, value]) => value > 0) + .map(([key, value]) => ({ name: key, value })) + ); - const totalClients = filteredClients.length; - - const transformData = (labels, key) => - labels - .map((label) => ({ - name: label, - value: filteredClients.filter((c) => c[key] === label).length, - })) - .filter((d) => d.value > 0); - - const genderData = transformData(genders, "gender"); - const regionData = transformData(regions, "region"); - - const ageData = ageGroups - .map((group, index) => { - const minAge = index * 10 + 10; - const maxAge = index === 5 ? 100 : minAge + 9; - return { - name: group, - value: filteredClients.filter( - (client) => client.age >= minAge && client.age <= maxAge - ).length, + setRegionData( + Object.entries(regionStats).map(([key, value]) => ({ + name: key, + value, + })) + ); + + const convertAgeKeyToKorean = (key) => { + if (key.includes("10.0-20.0")) return "10λŒ€"; + if (key.includes("20.0-30.0")) return "20λŒ€"; + if (key.includes("30.0-40.0")) return "30λŒ€"; + if (key.includes("40.0-50.0")) return "40λŒ€"; + if (key.includes("50.0-60.0")) return "50λŒ€"; + if (key.includes("60.0-*")) return "60λŒ€ 이상"; + return key; }; - }) - .filter((d) => d.value > 0); + const ageOrder = ["10λŒ€", "20λŒ€", "30λŒ€", "40λŒ€", "50λŒ€", "60λŒ€ 이상"]; + + const formattedAgeData = Object.entries(ageStats) + .map(([key, value]) => ({ + name: convertAgeKeyToKorean(key), + value, + })) + .filter(({ value }) => value > 0) + .sort((a, b) => ageOrder.indexOf(a.name) - ageOrder.indexOf(b.name)); + + setAgeData(formattedAgeData); + } catch (error) { + console.error("❌ [ERROR] 데이터 뢈러였기 μ‹€νŒ¨:", error); + } + setLoading(false); + }, [filterGender, filterRegion, filterAgeGroup]); + + useEffect(() => { + fetchUserStats(); + }, [fetchUserStats]); const handleFilterChange = (setFilter, value) => { setFilter((prev) => { @@ -117,6 +186,12 @@ const ClientRetention = () => { }); }; + const resetFilters = () => { + setFilterGender(["전체"]); + setFilterRegion(["전체"]); + setFilterAgeGroup(["전체"]); + }; + const toggleDropdown = (type) => { setDropdownOpen((prev) => ({ ...prev, [type]: !prev[type] })); }; @@ -130,7 +205,15 @@ const ClientRetention = () => { {dropdownOpen[type] && (
- {["전체", ...options].map((option) => ( + + {options.map((option) => (
); }; From 94318bff399d587570794f33804a5b4835a2eef4 Mon Sep 17 00:00:00 2001 From: LeeJeongHoon <142333228+DawnIsProblem@users.noreply.github.com> Date: Tue, 18 Feb 2025 10:59:49 +0900 Subject: [PATCH 18/18] =?UTF-8?q?chore:=20API=5FBASE=5FURL=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=EB=B3=80=EC=88=98=ED=99=94=20=EB=B0=8F=20=EC=A0=84?= =?UTF-8?q?=ED=99=94=EB=B2=88=ED=98=B8=20=EA=B2=80=EC=A6=9D=20=EB=B0=8F=20?= =?UTF-8?q?=EA=B3=A0=EA=B0=9D=20=EC=9C=84=EC=B9=98=20=EC=88=98=EC=A0=95=20?= =?UTF-8?q?=EC=8B=9C=20=EC=A6=89=EA=B0=81=20=EC=A7=80=EB=8F=84=EC=97=90=20?= =?UTF-8?q?=EB=B0=98=EC=98=81=EB=90=98=EA=B2=8C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/ClientManagement.js | 760 ++++++++++++++++++++++++----- 1 file changed, 643 insertions(+), 117 deletions(-) diff --git a/src/components/ClientManagement.js b/src/components/ClientManagement.js index 5faf989..9fc7d1e 100644 --- a/src/components/ClientManagement.js +++ b/src/components/ClientManagement.js @@ -1,8 +1,10 @@ import "../styles/ClientManagement.css"; -import DummyData from "../data/DummyData"; import React, { useState, useEffect } from "react"; import KakaoMap from "./KakaoMap"; +const API_BASE_URL = process.env.REACT_APP_API_URL; +const REST_API_KEY = process.env.REACT_APP_KAKAO_REST_API_KEY; + const ClientManagement = () => { const [selectedClient, setSelectedClient] = useState(null); const [searchTerm, setSearchTerm] = useState(""); @@ -13,8 +15,15 @@ const ClientManagement = () => { const [isEditModalOpen, setIsEditModalOpen] = useState(false); const [isHistoryModalOpen, setIsHistoryModalOpen] = useState(false); const [editedClient, setEditedClient] = useState(null); - const [clients, setClients] = useState(DummyData); - const [isKakaoLoaded, setIsKakaoLoaded] = useState(!!window.kakao?.maps); + const [clients, setClients] = useState([]); + const [isKakaoLoaded] = useState(!!window.kakao?.maps); + const [totalPages, setTotalPages] = useState(1); + const [currentPage, setCurrentPage] = useState([]); + const [pageGroup, setPageGroup] = useState(0); + const [setAddresses] = useState([]); + const [addressHistory, setAddressHistory] = useState([]); + const itemsPerPage = 10; + const pagesPerGroup = 10; const [newClient, setNewClient] = useState({ name: "", @@ -22,91 +31,459 @@ const ClientManagement = () => { phone: "", gender: "", age: "", - region_Address: "", - road_Address: "", - lat: null, - lng: null, + regionAddress: "", + roadAddress: "", addressHistory: [], }); + const fetchClients = async () => { + try { + let token = localStorage.getItem("token")?.trim(); + if (!token) throw new Error("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€."); + + if (!token.startsWith("Bearer ")) { + token = `Bearer ${token}`; + } + + const response = await fetch( + `${API_BASE_URL}/api/users?page=1&size=${itemsPerPage}`, + { + method: "GET", + headers: { Authorization: token }, + } + ); + + if (!response.ok) throw new Error("데이터 뢈러였기 μ‹€νŒ¨"); + + const data = await response.json(); + + console.log("πŸ“Œ μ„œλ²„ 응닡 데이터:", data); + + setClients(data.content || data.contents || []); + setTotalPages(data.totalPages || data.pageable?.totalPages || 1); + setCurrentPage(1); + } catch (error) { + console.error("🚨 고객 데이터 κ°€μ Έμ˜€κΈ° 였λ₯˜:", error); + alert(error.message); + } + }; + + const searchClients = async () => { + if (!searchTerm.trim()) { + fetchClients(); + return; + } + + try { + const token = localStorage.getItem("token"); + if (!token) throw new Error("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€."); + + let endpoint = ""; + let queryParam = ""; + + if (searchCategory === "name") { + endpoint = `/api/users/search/name`; + queryParam = `name=${searchTerm}`; + } else if (searchCategory === "newAddress") { + endpoint = `/api/users/search/road`; + queryParam = `keyword=${searchTerm}`; + } else if (searchCategory === "oldAddress") { + endpoint = `/api/users/search/region`; + queryParam = `keyword=${searchTerm}`; + } + + const response = await fetch( + `${API_BASE_URL}${endpoint}?${queryParam}&page=1&size=${itemsPerPage}`, + { + method: "GET", + headers: { Authorization: token }, + } + ); + + if (!response.ok) throw new Error("검색 μ‹€νŒ¨"); + + const data = await response.json(); + + console.log("πŸ” 검색 κ²°κ³Ό 응닡 데이터:", data); + + const updatedClients = (data.content || data.contents || []).map( + (client) => ({ + ...client, + regionAddress: client.regionAddress || "μ£Όμ†Œ μ—†μŒ", + roadAddress: client.roadAddress || "μ£Όμ†Œ μ—†μŒ", + }) + ); + + setClients(updatedClients); + setTotalPages(data.totalPages || data.pageable?.totalPages || 1); + setCurrentPage(1); + } catch (error) { + console.error("🚨 검색 쀑 였λ₯˜ λ°œμƒ:", error); + alert(error.message); + } + }; + + const filteredClients = searchTerm.trim() ? clients : clients; + useEffect(() => { - if (window.kakao && window.kakao.maps) { - console.log("βœ… Kakao 지도 APIκ°€ 이미 λ‘œλ“œλ¨"); - setIsKakaoLoaded(true); + console.log("🟒 ν΄λΌμ΄μ–ΈνŠΈ 리슀트 μ—…λ°μ΄νŠΈ:", clients); + }, [clients]); + + useEffect(() => { + console.log("πŸ” ν•„ν„°λ§λœ 고객 리슀트:", filteredClients); + }, [filteredClients]); + + const handleSearchInputChange = (e) => { + setSearchTerm(e.target.value); + }; + + const handleSearchKeyPress = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + searchClients(); + } + }; + + const handleShowAllClients = () => { + fetchClients(); + setSearchTerm(""); + }; + + const handleClientSelect = async (client) => { + console.log("πŸ“Œ μ„ νƒλœ 고객:", client); + + let updatedClient = { + id: client.id, + name: client.name, + email: client.email, + phoneNumber: client.phoneNumber || client.phone || "", + regionAddress: + client.latestRegionAddress || client.regionAddress || "μ£Όμ†Œ μ—†μŒ", + roadAddress: + client.latestRoadAddress || client.roadAddress || "μ£Όμ†Œ μ—†μŒ", + gender: client.gender, + age: client.age, + lat: client.x || client.lat || null, + lng: client.y || client.lng || null, + }; + + let addressToSearch = + updatedClient.roadAddress !== "μ£Όμ†Œ μ—†μŒ" + ? updatedClient.roadAddress + : updatedClient.regionAddress; + + if (!updatedClient.lat || !updatedClient.lng) { + console.log("πŸ“ 지도 μ’Œν‘œ μ—†μŒ, Kakao API둜 λ³€ν™˜ μ‹œλ„..."); + + try { + let coords = await fetchCoordinates(addressToSearch); + console.log("πŸ“Œ λ³€ν™˜λœ Kakao μ’Œν‘œ:", coords); + + if (coords.lat && coords.lng) { + updatedClient.lat = coords.lat; + updatedClient.lng = coords.lng; + } else { + console.warn("⚠️ Kakao API μ’Œν‘œ λ³€ν™˜ μ‹€νŒ¨. κΈ°λ³Έκ°’ μœ μ§€"); + } + + console.log("🟒 μ΅œμ’… μ—…λ°μ΄νŠΈλœ μ’Œν‘œ:", updatedClient); + } catch (error) { + console.error("🚨 Kakao API μ’Œν‘œ λ³€ν™˜ 쀑 였λ₯˜ λ°œμƒ:", error); + } } + + setSelectedClient(updatedClient); + }; + + useEffect(() => { + fetchClients(); }, []); - const filteredClients = clients.filter((client) => - client[searchCategory].toLowerCase().includes(searchTerm.toLowerCase()) - ); + const fetchClientsAndAddresses = async () => { + try { + const token = localStorage.getItem("token"); + if (!token) { + throw new Error("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€."); + } + + const userResponse = await fetch( + `${API_BASE_URL}/api/users?page=1&size=${itemsPerPage}`, + { + method: "GET", + headers: { Authorization: token }, + } + ); - const handleSearchAddress = (setClientState) => { + if (!userResponse.ok) { + throw new Error("고객 데이터 뢈러였기 μ‹€νŒ¨"); + } + + const userData = await userResponse.json(); + setClients(userData.contents || []); + + const newAddress = { + regionAddress: newClient.regionAddress || "", + roadAddress: newClient.roadAddress || "", + x: newClient.lng ? Number(newClient.lng) : null, + y: newClient.lat ? Number(newClient.lat) : null, + region: newClient.region || "μ„œμšΈ", + }; + + console.log("πŸ“Œ POST /api/addresses μš”μ²­ 데이터:", newAddress); + + if (!newAddress.roadAddress || !newAddress.regionAddress) { + console.warn("⚠️ μ£Όμ†Œ 정보가 λΆˆμ™„μ „ν•˜μ—¬ μš”μ²­μ„ 보내지 μ•ŠμŒ."); + return; + } + + const addressResponse = await fetch(`${API_BASE_URL}/api/addresses`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: token, + }, + body: JSON.stringify(newAddress), + }); + + const responseText = await addressResponse.text(); + console.log("πŸ”΄ μ£Όμ†Œ API 응닡:", responseText); + + if (!addressResponse.ok) { + console.error(`🚨 μ£Όμ†Œ μ €μž₯ μ‹€νŒ¨ (HTTP ${addressResponse.status})`); + throw new Error(responseText); + } + + const addressData = JSON.parse(responseText); + setAddresses(addressData.contents || []); + + console.log("πŸ“Œ κ°€μ Έμ˜¨ μ£Όμ†Œ 데이터:", addressData.contents); + } catch (error) { + console.error("🚨 데이터 κ°€μ Έμ˜€λŠ” 쀑 였λ₯˜ λ°œμƒ:", error); + alert(error.message); + } + }; + + useEffect(() => { + fetchClientsAndAddresses(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const handlePageChange = async (pageNumber) => { + setCurrentPage(pageNumber); + + try { + const token = localStorage.getItem("token"); + if (!token) throw new Error("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€."); + + const response = await fetch( + `${API_BASE_URL}/api/users?page=${pageNumber}&size=${itemsPerPage}`, + { + method: "GET", + headers: { Authorization: token }, + } + ); + + if (!response.ok) throw new Error("데이터 뢈러였기 μ‹€νŒ¨"); + + const data = await response.json(); + setClients(data.contents); + setTotalPages(data.pageable?.totalPages || 1); + } catch (error) { + console.error("🚨 νŽ˜μ΄μ§€ λ³€κ²½ 였λ₯˜:", error); + } + }; + + const handlePrevPageGroup = () => { + if (pageGroup > 0) { + setPageGroup(pageGroup - 1); + } + }; + + const handleNextPageGroup = () => { + if ((pageGroup + 1) * pagesPerGroup < totalPages) { + setPageGroup(pageGroup + 1); + } + }; + + const pageNumbers = Array.from( + { length: pagesPerGroup }, + (_, i) => pageGroup * pagesPerGroup + i + 1 + ).filter((page) => page <= totalPages); + + const handleSearchAddressForNewClient = () => { new window.daum.Postcode({ oncomplete: function (data) { const { roadAddress, jibunAddress, autoJibunAddress } = data; + console.log("πŸ“ [μΆ”κ°€] μ„ νƒλœ μ£Όμ†Œ:", roadAddress, jibunAddress); + + setNewClient((prev) => { + const updatedClient = { + ...prev, + roadAddress: roadAddress || "", + regionAddress: jibunAddress || autoJibunAddress || "", + }; + console.log("🟒 [μΆ”κ°€] μ—…λ°μ΄νŠΈλœ newClient μƒνƒœ:", updatedClient); + return updatedClient; + }); + }, + }).open(); + }; - setClientState((prev) => ({ - ...prev, - road_Address: roadAddress || prev.road_Address, - region_Address: - jibunAddress || autoJibunAddress || prev.region_Address, - })); - - const addressForGeocode = - roadAddress || jibunAddress || autoJibunAddress; - if (addressForGeocode) { - fetchCoordinates(addressForGeocode, setClientState); - } + const handleSearchAddressForEditClient = () => { + new window.daum.Postcode({ + oncomplete: function (data) { + const { roadAddress, jibunAddress, autoJibunAddress } = data; + console.log("πŸ“ [μˆ˜μ •] μ„ νƒλœ μ£Όμ†Œ:", roadAddress, jibunAddress); + + setEditedClient((prev) => { + const updatedClient = { + ...prev, + roadAddress: roadAddress || "", + regionAddress: jibunAddress || autoJibunAddress || "", + }; + console.log("🟒 [μˆ˜μ •] μ—…λ°μ΄νŠΈλœ editedClient μƒνƒœ:", updatedClient); + return updatedClient; + }); }, }).open(); }; - const fetchCoordinates = (address, updateClientState) => { - const geocoder = new window.kakao.maps.services.Geocoder(); - geocoder.addressSearch(address, (result, status) => { - if (status === window.kakao.maps.services.Status.OK) { - updateClientState((prev) => ({ - ...prev, - lat: parseFloat(result[0].y), - lng: parseFloat(result[0].x), - })); + const fetchCoordinates = async (address) => { + return new Promise(async (resolve, reject) => { + if (!REST_API_KEY) { + console.error("🚨 Kakao REST API ν‚€κ°€ μ„€μ •λ˜μ§€ μ•Šμ•˜μŠ΅λ‹ˆλ‹€!"); + reject("Kakao API ν‚€ 였λ₯˜"); + return; + } + + console.log(`πŸ“‘ Kakao API μ£Όμ†Œ 검색 μš”μ²­: ${address}`); + + const fetchFromKakaoAPI = async (query) => { + const response = await fetch( + `https://dapi.kakao.com/v2/local/search/address.json?query=${encodeURIComponent( + query.trim() + )}`, + { + method: "GET", + headers: { + Authorization: `KakaoAK ${REST_API_KEY}`, + "Content-Type": "application/json", + }, + } + ); + return response.json(); + }; + + try { + let data = await fetchFromKakaoAPI(address); + + if (!data.documents || data.documents.length === 0) { + console.warn("⚠️ λ„λ‘œλͺ… μ£Όμ†Œ λ³€ν™˜ μ‹€νŒ¨. μ§€λ²ˆ μ£Όμ†Œλ‘œ μž¬μ‹œλ„..."); + data = await fetchFromKakaoAPI(address); + } + + if (data.documents && data.documents.length > 0) { + const coords = { + lat: parseFloat(data.documents[0].y), + lng: parseFloat(data.documents[0].x), + }; + console.log( + `πŸ“Œ λ³€ν™˜λœ Kakao μ’Œν‘œ: lat=${coords.lat}, lng=${coords.lng}` + ); + resolve(coords); + } else { + console.warn("⚠️ Kakao API 응닡 μ—†μŒ. κΈ°λ³Έ μ’Œν‘œ λ°˜ν™˜"); + resolve({ lat: 37.5665, lng: 126.978 }); + } + } catch (error) { + console.error("🚨 Kakao API μš”μ²­ μ‹€νŒ¨", error); + resolve({ lat: 37.5665, lng: 126.978 }); } }); }; - const handleAddClient = (e) => { + const handleAddClient = async (e) => { e.preventDefault(); - if (Object.values(newClient).some((field) => field === "")) { - alert("λͺ¨λ“  ν•„λ“œλ₯Ό μž…λ ₯ν•˜μ„Έμš”!"); + + if (!newClient.name || !newClient.email || !newClient.phone) { + alert("이름, 이메일, μ „ν™”λ²ˆν˜ΈλŠ” ν•„μˆ˜ μž…λ ₯ ν•­λͺ©μž…λ‹ˆλ‹€."); return; } - setClients([...clients, { id: clients.length + 1, ...newClient }]); - setNewClient({ - name: "", - email: "", - phone: "", - gender: "", - age: "", - region_Address: "", - road_Address: "", - lat: null, - lng: null, - }); - setIsModalOpen(false); + + console.log("πŸ“Œ μΆ”κ°€ν•  고객 정보 (전솑 μ „):", newClient); + + const userData = { + name: newClient.name, + email: newClient.email, + phoneNumber: newClient.phone.replace(/-/g, ""), + regionAddress: newClient.regionAddress || null, + roadAddress: newClient.roadAddress || null, + gender: newClient.gender, + age: parseInt(newClient.age, 10), + }; + + try { + const token = localStorage.getItem("token")?.trim(); + if (!token) throw new Error("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€."); + + const response = await fetch(`${API_BASE_URL}/api/users`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: token.startsWith("Bearer ") + ? token + : `Bearer ${token}`, + }, + body: JSON.stringify(userData), + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error("🚨 μ‚¬μš©μž μΆ”κ°€ 였λ₯˜ 응닡:", errorText); + + if (response.status === 409) { + alert("❌ 이미 μ‘΄μž¬ν•˜λŠ” μ΄λ©”μΌμž…λ‹ˆλ‹€."); + } else { + alert(`❌ μ‚¬μš©μž μΆ”κ°€ μ‹€νŒ¨ (HTTP ${response.status})`); + } + + throw new Error(`❌ μ‚¬μš©μž μΆ”κ°€ μ‹€νŒ¨ (HTTP ${response.status})`); + } + + console.log("βœ… μ‚¬μš©μž μΆ”κ°€ 성곡"); + alert("고객이 μ„±κ³΅μ μœΌλ‘œ μΆ”κ°€λ˜μ—ˆμŠ΅λ‹ˆλ‹€."); + fetchClients(); + } catch (error) { + console.error("🚨 μ‚¬μš©μž μΆ”κ°€ 쀑 였λ₯˜ λ°œμƒ:", error); + } }; useEffect(() => { if (selectedClient) { - console.log("πŸ“Œ μ„ νƒλœ 고객:", selectedClient); - console.log("πŸ“ 고객 μ’Œν‘œ:", selectedClient.lat, selectedClient.lng); console.log( - `πŸ“Œ lat νƒ€μž…: ${typeof selectedClient.lat}, lng νƒ€μž…: ${typeof selectedClient.lng}` + "πŸ“ 고객 μ’Œν‘œ μ—…λ°μ΄νŠΈλ¨:", + selectedClient.lat, + selectedClient.lng + ); + + console.log( + "πŸ“Œ μ£Όμ†Œ 정보:", + selectedClient.road_Address, + selectedClient.region_Address ); - if (typeof selectedClient.lat !== "number") { - console.warn("⚠️ lat λ˜λŠ” lng이 μˆ«μžκ°€ μ•„λ‹™λ‹ˆλ‹€. λ³€ν™˜ μ‹œλ„..."); - selectedClient.lat = parseFloat(selectedClient.lat); - selectedClient.lng = parseFloat(selectedClient.lng); + if ( + !selectedClient.lat || + !selectedClient.lng || + isNaN(selectedClient.lat) || + isNaN(selectedClient.lng) + ) { + console.warn("⚠️ 고객 μ’Œν‘œ μ—†μŒ. κΈ°λ³Έκ°’(μ„œμšΈ μ‹œμ²­) 적용."); + setSelectedClient((prev) => ({ + ...prev, + lat: 37.5665, + lng: 126.978, + })); } } }, [selectedClient]); @@ -142,8 +519,25 @@ const ClientManagement = () => { } }; + const handleOpenAddModal = () => { + setNewClient({ + name: "", + email: "", + phone: "", + gender: "", + age: "", + regionAddress: "", + roadAddress: "", + addressHistory: [], + }); + setIsModalOpen(true); + }; + const handleOpenEditModal = () => { - setEditedClient({ ...selectedClient }); + setEditedClient({ + ...selectedClient, + gender: selectedClient.gender === "λ‚¨μž" ? "MALE" : "FEMALE", + }); setIsEditModalOpen(true); }; @@ -156,18 +550,98 @@ const ClientManagement = () => { setEditedClient((prev) => ({ ...prev, [name]: value })); }; - const handleSaveEdit = () => { - setClients((prevClients) => - prevClients.map((client) => - client.id === editedClient.id ? editedClient : client - ) - ); - setSelectedClient(editedClient); - setIsEditModalOpen(false); - }; + const handleSaveEdit = async () => { + try { + let token = localStorage.getItem("token")?.trim(); - const handleOpenHistoryModal = () => { - setIsHistoryModalOpen(true); + if (!token) { + alert("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€."); + return; + } + + if (!token.startsWith("Bearer ")) { + token = `Bearer ${token}`; + } + + console.log("πŸ” μ΅œμ’… JWT Token:", `"${token}"`); + + let transformedGender = editedClient.gender; + if (transformedGender === "MALE") { + transformedGender = "λ‚¨μž"; + } else if (transformedGender === "FEMALE") { + transformedGender = "μ—¬μž"; + } + + const requestBody = { + name: editedClient.name, + email: editedClient.email, + phoneNumber: editedClient.phoneNumber, + regionAddress: + editedClient.regionAddress || selectedClient?.regionAddress, + roadAddress: editedClient.roadAddress || selectedClient?.roadAddress, + gender: transformedGender, + age: editedClient.age, + }; + + console.log( + "πŸ“€ 고객 μˆ˜μ • μš”μ²­ 데이터:", + JSON.stringify(requestBody, null, 2) + ); + + const response = await fetch( + `${API_BASE_URL}/api/users/${editedClient.id}`, + { + method: "PATCH", + headers: { + "Content-Type": "application/json", + Authorization: token, + }, + body: JSON.stringify(requestBody), + } + ); + + if (!response.ok) { + const errorText = await response.text(); + console.error("🚨 μ„œλ²„ 응닡 였λ₯˜:", errorText); + alert(`❌ μ„œλ²„ 응닡 였λ₯˜: ${errorText}`); + throw new Error(`❌ 고객 정보 μˆ˜μ • μ‹€νŒ¨ (status: ${response.status})`); + } + + console.log("βœ… 고객 정보 μˆ˜μ • 성곡!"); + + let updatedLat = selectedClient.lat; + let updatedLng = selectedClient.lng; + + if ( + editedClient.roadAddress !== selectedClient.roadAddress || + editedClient.regionAddress !== selectedClient.regionAddress + ) { + console.log("πŸ“Œ μ£Όμ†Œκ°€ 변경됨! Kakao API둜 μƒˆ μ’Œν‘œ μš”μ²­"); + + const newCoords = await fetchCoordinates( + editedClient.roadAddress || editedClient.regionAddress + ); + console.log("πŸ“Œ μƒˆ μ’Œν‘œ:", newCoords); + + if (newCoords.lat && newCoords.lng) { + updatedLat = newCoords.lat; + updatedLng = newCoords.lng; + } + } + + setSelectedClient((prev) => ({ + ...prev, + ...editedClient, + lat: updatedLat, + lng: updatedLng, + regionAddress: editedClient.regionAddress || prev.regionAddress, + roadAddress: editedClient.roadAddress || prev.roadAddress, + })); + + setIsEditModalOpen(false); + } catch (error) { + console.error("🚨 고객 정보 μˆ˜μ • 쀑 였λ₯˜ λ°œμƒ:", error); + } }; const handleCloseHistoryModal = (e) => { @@ -176,6 +650,38 @@ const ClientManagement = () => { } }; + const fetchAddressHistory = async (userId) => { + try { + const token = localStorage.getItem("token")?.trim(); + if (!token) throw new Error("둜그인이 ν•„μš”ν•©λ‹ˆλ‹€."); + + const response = await fetch( + `${API_BASE_URL}/api/users/${userId}/address-histories`, + { + method: "GET", + headers: { Authorization: token }, + } + ); + + if (!response.ok) throw new Error("μ£Όμ†Œ λ³€κ²½ λ‚΄μ—­ 뢈러였기 μ‹€νŒ¨"); + + const data = await response.json(); + console.log("πŸ“Œ μ£Όμ†Œ λ³€κ²½ λ‚΄μ—­:", data); + + setAddressHistory(data.responses || []); + } catch (error) { + console.error("🚨 μ£Όμ†Œ λ³€κ²½ λ‚΄μ—­ κ°€μ Έμ˜€κΈ° 였λ₯˜:", error); + setAddressHistory([]); + } + }; + + const handleOpenHistoryModal = () => { + if (selectedClient) { + fetchAddressHistory(selectedClient.id); + setIsHistoryModalOpen(true); + } + }; + return (

고객 관리

@@ -190,11 +696,15 @@ const ClientManagement = () => { setSearchTerm(e.target.value)} + onChange={handleSearchInputChange} + onKeyPress={handleSearchKeyPress} /> - +
@@ -206,7 +716,7 @@ const ClientManagement = () => {
setSelectedClient(client)} + onClick={() => handleClientSelect(client)} > {client.name}
@@ -214,6 +724,26 @@ const ClientManagement = () => {
+ {totalPages > 1 && ( +
+ {pageGroup > 0 && } + + {pageNumbers.map((page) => ( + + ))} + + {(pageGroup + 1) * pagesPerGroup < totalPages && ( + + )} +
+ )} + {selectedClient && (
@@ -242,13 +772,13 @@ const ClientManagement = () => { 이메일: {selectedClient.email}

- μ „ν™”λ²ˆν˜Έ: {selectedClient.phone} + μ „ν™”λ²ˆν˜Έ: {selectedClient.phoneNumber}

- λ„λ‘œλͺ… μ£Όμ†Œ: {selectedClient.road_Address} + λ„λ‘œλͺ… μ£Όμ†Œ: {selectedClient.roadAddress}

- μ§€λ²ˆ μ£Όμ†Œ: {selectedClient.region_Address} + μ§€λ²ˆ μ£Όμ†Œ: {selectedClient.regionAddress}

-
-

μ£Όμ†Œ 이전 λ‚΄μ—­

-
    - {selectedClient.addressHistory?.length > 0 ? ( - selectedClient.addressHistory.map((address, index) => ( -
  • {address}
  • +
    +

    πŸ“‹ μ£Όμ†Œ λ³€κ²½ λ‚΄μ—­

    + +
    +
      + {addressHistory.length > 0 ? ( + addressHistory.map((history, index) => ( +
    • +

      + λ„λ‘œλͺ… μ£Όμ†Œ:{" "} + {history.roadAddress || "μ—†μŒ"} +

      +

      + μ§€λ²ˆ μ£Όμ†Œ:{" "} + {history.regionAddress || "μ—†μŒ"} +

      +

      + λ³€κ²½ 일자: {history.createdAt} +

      +
    • )) ) : ( -

      이전 μ£Όμ†Œ 내역이 μ—†μŠ΅λ‹ˆλ‹€.

      +

      이전 μ£Όμ†Œ λ³€κ²½ 내역이 μ—†μŠ΅λ‹ˆλ‹€.

      )}
@@ -401,7 +935,6 @@ const ClientManagement = () => { } required /> - - { } required /> - { } required /> - { } required /> - - - -