From 8c29c5da9f9b02fbd0f8fcc99e3525edc98104a1 Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Tue, 14 Jan 2025 20:53:43 +0900 Subject: [PATCH 01/54] feat : slide card --- .../WebtoonCard/WebtoonCard.module.css | 2 -- .../components/WebtoonCard/WebtoonCard.tsx | 26 +++++++++++++--- .../WebtoonList/WebtoonList.module.css | 10 +++--- .../components/WebtoonList/WebtoonList.tsx | 31 ++++++++++--------- 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/WebtoonCard/WebtoonCard.module.css b/frontend/src/components/WebtoonCard/WebtoonCard.module.css index e77d7960..cecf4f4e 100644 --- a/frontend/src/components/WebtoonCard/WebtoonCard.module.css +++ b/frontend/src/components/WebtoonCard/WebtoonCard.module.css @@ -1,6 +1,4 @@ .webtoonCard { - width: 220px; - height: 360px; display: flex; flex-direction: column; align-items: center; diff --git a/frontend/src/components/WebtoonCard/WebtoonCard.tsx b/frontend/src/components/WebtoonCard/WebtoonCard.tsx index c4c6043d..08cc2747 100644 --- a/frontend/src/components/WebtoonCard/WebtoonCard.tsx +++ b/frontend/src/components/WebtoonCard/WebtoonCard.tsx @@ -7,15 +7,31 @@ import PlatformIcon from '../PlatformIcon/PlatformIcon'; interface WebtoonCardProps { webtoon: Webtoon; showTags?: boolean; + size?: number; } -const WebtoonCard: React.FC = ({ webtoon, showTags = true }) => { - const authors = webtoon.authors?.map(author => author.name).join(', ') || '작가 없음'; - const averageRating = webtoon.averageRating ? webtoon.averageRating.toFixed(1) : '0'; - const truncatedTitle = webtoon.title.length > 30 ? `${webtoon.title.substring(0, 30)}...` : webtoon.title; +const WebtoonCard: React.FC = ({ webtoon, showTags = true, size = 360 }) => { + const getAuthors = (authors: { name: string }[] | undefined): string => { + return authors?.map(author => author.name).join(', ') || '작가 없음'; + }; + + const formatAverageRating = (rating: number | undefined): string => { + return rating ? rating.toFixed(1) : '0'; + }; + + const truncateTitle = (title: string): string => { + return title.length > 30 ? `${title.substring(0, 30)}...` : title; + }; + + const authors = getAuthors(webtoon.authors); + const averageRating = formatAverageRating(webtoon.averageRating); + const truncatedTitle = truncateTitle(webtoon.title); + + const height = size; + const width = (size * 220) / 360; return ( - +
{webtoon.title} diff --git a/frontend/src/components/WebtoonList/WebtoonList.module.css b/frontend/src/components/WebtoonList/WebtoonList.module.css index 561f4a0d..8709f962 100644 --- a/frontend/src/components/WebtoonList/WebtoonList.module.css +++ b/frontend/src/components/WebtoonList/WebtoonList.module.css @@ -1,15 +1,17 @@ .webtoonList { - padding: 20px; + padding: 10px; + position: relative; + overflow: hidden; } .carousel { display: flex; - overflow: hidden; - scroll-behavior: smooth; + transition: transform 0.5s ease; + will-change: transform; } .carousel > * { - flex: 0 0 20%; + flex: 0 0 auto; margin-right: 20px; } diff --git a/frontend/src/components/WebtoonList/WebtoonList.tsx b/frontend/src/components/WebtoonList/WebtoonList.tsx index c12e6947..117874b8 100644 --- a/frontend/src/components/WebtoonList/WebtoonList.tsx +++ b/frontend/src/components/WebtoonList/WebtoonList.tsx @@ -10,7 +10,7 @@ interface WebtoonListProps { const WebtoonList: React.FC = ({ webtoons, showTags = true }) => { const [currentPage, setCurrentPage] = useState(0); - const itemsPerPage = 5; + const itemsPerPage = 4; const totalPages = Math.ceil(webtoons.length / itemsPerPage); const handleNext = () => { @@ -28,21 +28,24 @@ const WebtoonList: React.FC = ({ webtoons, showTags = true }) const startIndex = currentPage * itemsPerPage; const currentWebtoons = webtoons.slice(startIndex, startIndex + itemsPerPage); + const cardHeight = 360; + const cardWidth = ((cardHeight * 220) / 360 + 20) * 4; + return (
- {currentWebtoons.length > 0 ? ( -
- {currentWebtoons.map((webtoon) => ( - - ))} -
- ) : ( -

웹툰이 없습니다.

- )} +
+ {webtoons.map((webtoon) => ( + + ))} +
void; onLogout: () => void; isWidgetOpen: boolean; @@ -14,9 +13,7 @@ interface ProfileWidgetProps { } const ProfileWidget: React.FC = ({ - userProfilePic, - userName, - userEmail, + memberProfile, onNavigate, onLogout, isWidgetOpen, @@ -45,6 +42,8 @@ const ProfileWidget: React.FC = ({ }; }, [handleClickOutside]); + const defaultProfilePicture = '/image/profile/user.png'; + return (
= ({ style={{ pointerEvents: isWidgetOpen ? 'auto' : 'none' }} ref={profileWidgetRef} > - User Profile + User Profile
-

{userName}

-

{userEmail}

+

{memberProfile?.nickname || '게스트 사용자'}

- - - - -
+ -
+
{isLoggedIn ? ( <> void; +} + +const Menu: React.FC = ({ navigate }) => { + return ( + + ); +}; + +export default Menu; \ No newline at end of file diff --git a/frontend/src/components/Header/Search.tsx b/frontend/src/components/Header/Search.tsx new file mode 100644 index 00000000..531965f2 --- /dev/null +++ b/frontend/src/components/Header/Search.tsx @@ -0,0 +1,54 @@ +import React, { useState, useRef } from 'react'; +import { FiSearch, FiSun, FiMoon, FiBell } from 'react-icons/fi'; +import styles from './Header.module.css'; + +interface SearchProps { + navigate: (path: string) => void; + isDarkTheme: boolean; + toggleTheme: () => void; +} + +const Search: React.FC = ({ navigate, isDarkTheme, toggleTheme }) => { + const [isSearchInputVisible, setSearchInputVisible] = useState(false); + const searchInputRef = useRef(null); + + const toggleSearchInput = (event: React.MouseEvent): void => { + event.stopPropagation(); + setSearchInputVisible((prev) => !prev); + }; + + return ( +
+ {isSearchInputVisible && ( + ) => { + if (e.key === 'Enter') { + navigate(`/search?query=${e.currentTarget.value}`); + } + }} + /> + )} + + + + + +
+ ); +}; + +export default Search; \ No newline at end of file diff --git a/frontend/src/components/ProfileWidget/ProfileWidget.tsx b/frontend/src/components/ProfileWidget/ProfileWidget.tsx index 3b2ff20f..5323ddd6 100644 --- a/frontend/src/components/ProfileWidget/ProfileWidget.tsx +++ b/frontend/src/components/ProfileWidget/ProfileWidget.tsx @@ -21,8 +21,13 @@ const ProfileWidget: React.FC = ({ profileButtonRef, profileWidgetRef, }) => { - const handleButtonClick = (action: () => void) => { + const handleButtonClick = (action: () => void, closeWidget: () => void) => (event: React.MouseEvent) => { + event.stopPropagation(); action(); + closeWidget(); + }; + + const closeProfileWidget = () => { setProfileWidgetOpen(false); }; @@ -31,7 +36,7 @@ const ProfileWidget: React.FC = ({ const isProfileWidgetClick = profileWidgetRef.current && profileWidgetRef.current.contains(event.target as Node); if (!isProfileClick && !isProfileWidgetClick) { - setProfileWidgetOpen(false); + closeProfileWidget(); } }, [profileButtonRef, profileWidgetRef, setProfileWidgetOpen]); @@ -60,25 +65,25 @@ const ProfileWidget: React.FC = ({

{memberProfile?.nickname || '게스트 사용자'}

@@ -77,7 +82,6 @@ const SignInPage: React.FC = () => { placeholder="비밀번호" value={formData.password} onChange={handleChange} - required />
@@ -101,9 +105,9 @@ const SignInPage: React.FC = () => {
- handleSocialLogin('google')} /> - handleSocialLogin('kakao')} /> - handleSocialLogin('naver')} /> + handleSocialLogin('google')} type="button" /> + handleSocialLogin('kakao')} type="button" /> + handleSocialLogin('naver')} type="button" />
From cd77be024733e874933d8ef09da2b41fd9c548a5 Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Sat, 25 Jan 2025 22:36:40 +0900 Subject: [PATCH 16/54] =?UTF-8?q?feat:=20Profile=20Page=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AchievementItem.module.css | 23 +++-- .../AchievementItem/AchievementItem.tsx | 4 + .../LevelDisplay/LevelDisplay.module.css | 16 +-- .../WebtoonList/WebtoonList.module.css | 15 ++- .../components/WebtoonList/WebtoonList.tsx | 47 +++++---- .../MemberProfileSection.module.css | 98 ++++++++++++++++--- .../MemberProfileSection.tsx | 61 ++++++++---- 7 files changed, 191 insertions(+), 73 deletions(-) diff --git a/frontend/src/components/AchievementItem/AchievementItem.module.css b/frontend/src/components/AchievementItem/AchievementItem.module.css index 163c6d88..1f38f746 100644 --- a/frontend/src/components/AchievementItem/AchievementItem.module.css +++ b/frontend/src/components/AchievementItem/AchievementItem.module.css @@ -1,22 +1,24 @@ .achievementItem { display: flex; align-items: center; - background-color: #f9f9f9; + background-color: #ffffff; border: 1px solid #ddd; - border-radius: 8px; - padding: 10px; + border-radius: 12px; + padding: 15px; margin: 10px 0; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); - transition: transform 0.2s; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; + cursor: pointer; } .achievementItem:hover { - transform: scale(1.02); + transform: scale(1.05); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); } .icon { - font-size: 24px; - margin-right: 10px; + font-size: 28px; + margin-right: 15px; } .details { @@ -25,11 +27,12 @@ } .title { - font-size: 16px; + font-size: 18px; + font-weight: bold; color: #333; } .count { - font-size: 14px; + font-size: 16px; color: #666; } \ No newline at end of file diff --git a/frontend/src/components/AchievementItem/AchievementItem.tsx b/frontend/src/components/AchievementItem/AchievementItem.tsx index 584824d2..2e2a8477 100644 --- a/frontend/src/components/AchievementItem/AchievementItem.tsx +++ b/frontend/src/components/AchievementItem/AchievementItem.tsx @@ -1,5 +1,6 @@ import React from 'react'; import styles from './AchievementItem.module.css'; +import { FaTrophy } from 'react-icons/fa'; interface AchievementItemProps { title: string; @@ -9,6 +10,9 @@ interface AchievementItemProps { const AchievementItem: React.FC = ({ title, count }) => { return (
+
+ +
{title} {count} diff --git a/frontend/src/components/LevelDisplay/LevelDisplay.module.css b/frontend/src/components/LevelDisplay/LevelDisplay.module.css index 4ba175d1..f6af7706 100644 --- a/frontend/src/components/LevelDisplay/LevelDisplay.module.css +++ b/frontend/src/components/LevelDisplay/LevelDisplay.module.css @@ -1,27 +1,29 @@ .levelContainer { display: flex; align-items: center; - margin: 10px 0; + margin-top: 10px; + width: 100%; + max-width: 300px; } .progressBar { - width: 100%; - height: 10px; + flex-grow: 1; + height: 8px; background-color: #e0e0e0; - border-radius: 5px; + border-radius: 4px; overflow: hidden; margin-right: 10px; } .progress { height: 100%; - background-color: #4caf50; + background-color: #007bff; transition: width 0.3s ease; } .levelInfo { - display: flex; - align-items: center; + font-size: 14px; + color: #333; } .levelIcon { diff --git a/frontend/src/components/WebtoonList/WebtoonList.module.css b/frontend/src/components/WebtoonList/WebtoonList.module.css index 434d1ba0..ddc02804 100644 --- a/frontend/src/components/WebtoonList/WebtoonList.module.css +++ b/frontend/src/components/WebtoonList/WebtoonList.module.css @@ -2,6 +2,7 @@ padding: 10px; position: relative; overflow: hidden; + min-height: 220px; /* 기본 높이 설정 */ } .carousel { @@ -20,12 +21,17 @@ color: #888; font-size: 18px; margin-top: 20px; + width: 100%; } .navigation { display: flex; - justify-content: center; - margin-top: 10px; + justify-content: space-between; + position: absolute; + top: 50%; + width: 100%; + transform: translateY(-50%); + pointer-events: none; } .navButton { @@ -33,10 +39,9 @@ border: none; font-size: 24px; cursor: pointer; - margin: 0 10px; + pointer-events: auto; } .navButton:disabled { - cursor: not-allowed; - opacity: 0.5; + display: none; } \ No newline at end of file diff --git a/frontend/src/components/WebtoonList/WebtoonList.tsx b/frontend/src/components/WebtoonList/WebtoonList.tsx index dca6cd1f..2dadae92 100644 --- a/frontend/src/components/WebtoonList/WebtoonList.tsx +++ b/frontend/src/components/WebtoonList/WebtoonList.tsx @@ -11,7 +11,7 @@ interface WebtoonListProps { const WebtoonList: React.FC = ({ webtoons, size = 220, showTags = true }) => { const [currentPage, setCurrentPage] = useState(0); - const itemsPerPage = 4; + const itemsPerPage = 5; const totalPages = Math.ceil(webtoons.length / itemsPerPage); const handleNext = () => { @@ -26,30 +26,39 @@ const WebtoonList: React.FC = ({ webtoons, size = 220, showTag } }; - return (
- {webtoons.map((webtoon) => ( - - ))} -
-
- - + {webtoons.length > 0 ? ( + webtoons.map((webtoon) => ( + + )) + ) : ( +
웹툰이 없습니다.
+ )}
+ {webtoons.length > itemsPerPage && ( +
+ {currentPage > 0 && ( + + )} + {currentPage < totalPages - 1 && ( + + )} +
+ )}
); }; diff --git a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.module.css b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.module.css index c9ad2f24..61ac4552 100644 --- a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.module.css +++ b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.module.css @@ -1,12 +1,14 @@ .profile { margin: 20px 0; - padding: 20px; + padding: 30px; border-radius: 12px; background-color: #ffffff; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); transition: box-shadow 0.3s; display: flex; - flex-direction: column; + align-items: flex-start; + height: 270px; + width: 100%; } .profile:hover { @@ -15,23 +17,33 @@ .userInfo { display: flex; - align-items: center; + align-items: flex-start; + width: 100%; } .profilePicture { - width: 80px; - height: 80px; + width: 100px; + height: 100px; border-radius: 50%; - margin-right: 20px; + margin-right: 30px; + margin-left: 10px; + border: 2px solid #ddd; } .profileDetails { display: flex; flex-direction: column; + flex-grow: 1; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; } -.profileDetails h3 { - font-size: 24px; +.header h3 { + font-size: 28px; margin: 0; color: #333; } @@ -44,21 +56,77 @@ .stats { display: flex; - justify-content: space-between; + justify-content: space-around; margin-top: 10px; } .statItem { - background-color: #f0f0f0; - border-radius: 8px; - padding: 10px; - flex: 1; - margin: 0 5px; text-align: center; +} + +.statCount { + font-size: 18px; + font-weight: bold; + color: #333; +} + +.statLabel { + font-size: 14px; + color: #666; +} + +.achievements { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 15px; + align-items: center; +} + +.iconButton { + background: none; + border: none; + cursor: pointer; + margin-left: 10px; + font-size: 20px; + transition: color 0.3s; +} + +.iconButton:hover { + color: #007bff; +} + +.actions { + display: flex; + justify-content: flex-start; + margin-top: 15px; +} + +.followButton, .editProfileButton { + padding: 8px 16px; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + margin-right: 10px; + border: none; transition: background-color 0.3s; } -.statItem:hover { +.followButton { + background-color: #007bff; + color: white; +} + +.followButton:hover { + background-color: #0056b3; +} + +.editProfileButton { + background-color: #f0f0f0; + color: #333; +} + +.editProfileButton:hover { background-color: #e0e0e0; } diff --git a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx index de73e6b2..2be50a87 100644 --- a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx +++ b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx @@ -1,39 +1,66 @@ -import React from 'react'; +import React, { useState } from 'react'; import styles from './MemberProfileSection.module.css'; -import LevelDisplay from '@components/LevelDisplay'; -import AchievementItem from '@components/AchievementItem'; +import { useNavigate } from 'react-router-dom'; import { MemberProfile } from '@models/member'; +import { FaHeart, FaEdit, FaEllipsisH } from 'react-icons/fa'; +import AchievementItem from '@components/AchievementItem'; +import LevelDisplay from '@components/LevelDisplay'; interface MemberProfileSectionProps { memberProfile: MemberProfile | null; } const MemberProfileSection: React.FC = ({ memberProfile }) => { + const navigate = useNavigate(); + const [isFollowing, setIsFollowing] = useState(false); + const [followerCount, setFollowerCount] = useState(120); // 더미 데이터 + const [followingCount, setFollowingCount] = useState(150); // 더미 데이터 + const [recommendationCount, setRecommendationCount] = useState(30); // 더미 데이터 + const [reviewCount, setReviewCount] = useState(45); // 더미 데이터 + const [averageRating, setAverageRating] = useState(4.5); // 더미 데이터 + + const toggleFollow = () => { + setIsFollowing((prev) => !prev); + setFollowerCount((prev) => (isFollowing ? prev - 1 : prev + 1)); + }; + + const handleEditProfile = () => { + // TODO: 프로필 수정 기능 추가 + }; + + const handleMoreOptions = () => { + // todo: 더보기 기능 추가 + }; + return (
{memberProfile && (
프로필
-

{memberProfile.nickname}

+
+

{memberProfile.nickname}

+
+ + + +
+
-
- - - +
+ + +
From 37741c0593c6f551eb75a50d7224a77cf45a991d Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Sat, 25 Jan 2025 22:37:00 +0900 Subject: [PATCH 17/54] feat : add profile edit page --- frontend/src/App.tsx | 2 + .../MemberProfileSection.tsx | 1 + .../ProfileEditPage.module.css | 95 +++++++++++++++ .../pages/ProfileEditPage/ProfileEditPage.tsx | 36 ++++++ .../ProfilePictureUpload.module.css | 53 +++++++++ .../ProfileEditPage/ProfilePictureUpload.tsx | 54 +++++++++ .../ProfileEditPage/ProfileSection.module.css | 42 +++++++ .../pages/ProfileEditPage/ProfileSection.tsx | 54 +++++++++ .../SettingsSection.module.css | 42 +++++++ .../pages/ProfileEditPage/SettingsSection.tsx | 111 ++++++++++++++++++ frontend/src/pages/ProfileEditPage/index.ts | 1 + 11 files changed, 491 insertions(+) create mode 100644 frontend/src/pages/ProfileEditPage/ProfileEditPage.module.css create mode 100644 frontend/src/pages/ProfileEditPage/ProfileEditPage.tsx create mode 100644 frontend/src/pages/ProfileEditPage/ProfilePictureUpload.module.css create mode 100644 frontend/src/pages/ProfileEditPage/ProfilePictureUpload.tsx create mode 100644 frontend/src/pages/ProfileEditPage/ProfileSection.module.css create mode 100644 frontend/src/pages/ProfileEditPage/ProfileSection.tsx create mode 100644 frontend/src/pages/ProfileEditPage/SettingsSection.module.css create mode 100644 frontend/src/pages/ProfileEditPage/SettingsSection.tsx create mode 100644 frontend/src/pages/ProfileEditPage/index.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7d1bebf8..2e63e60f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,7 @@ import SocialLoginCallbackPage from '@pages/auth/SocialLoginCallbackPage'; import NewWebtoonsPage from '@pages/NewWebtoonsPage'; import OngoingWebtoonsPage from '@pages/OngoingWebtoonsPage'; import CompletedWebtoonsPage from '@pages/CompletedWebtoonsPage'; +import ProfileEditPage from '@pages/ProfileEditPage'; import ErrorPage from '@pages/ErrorPage/ErrorPage'; const App: React.FC = () => { @@ -33,6 +34,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx index 2be50a87..f277896a 100644 --- a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx +++ b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx @@ -26,6 +26,7 @@ const MemberProfileSection: React.FC = ({ memberProfi const handleEditProfile = () => { // TODO: 프로필 수정 기능 추가 + navigate('/profile/edit'); }; const handleMoreOptions = () => { diff --git a/frontend/src/pages/ProfileEditPage/ProfileEditPage.module.css b/frontend/src/pages/ProfileEditPage/ProfileEditPage.module.css new file mode 100644 index 00000000..f0c425d7 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfileEditPage.module.css @@ -0,0 +1,95 @@ +.container { + display: flex; + max-width: 1200px; + margin: 0 auto; + padding: 20px; + background-color: #f9f9f9; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +} + +.sidebar { + flex: 1; + padding: 20px; + background-color: #ffffff; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + margin-right: 20px; +} + +.sidebar h2 { + font-size: 20px; + margin-bottom: 15px; + color: #333; +} + +.sidebar ul { + list-style: none; + padding: 0; +} + +.sidebar li { + padding: 10px; + cursor: pointer; + transition: background-color 0.3s; +} + +.sidebar li:hover { + background-color: #e0e0e0; +} + +.active { + background-color: #007bff; + color: white; +} + +.content { + flex: 3; + padding: 20px; + background-color: #ffffff; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +h2 { + font-size: 24px; + margin-bottom: 20px; + color: #333; +} + +.profileSection, .settingsSection { + margin-bottom: 40px; +} + +label { + display: block; + margin-bottom: 15px; + font-size: 16px; + color: #666; +} + +.input, .select { + width: 100%; + padding: 10px; + margin-top: 5px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 16px; +} + +.saveButton { + display: block; + width: 100%; + padding: 12px; + background-color: #007bff; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.saveButton:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfileEditPage.tsx b/frontend/src/pages/ProfileEditPage/ProfileEditPage.tsx new file mode 100644 index 00000000..a837cdee --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfileEditPage.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import styles from './ProfileEditPage.module.css'; +import ProfileSection from './ProfileSection'; +import SettingsSection from './SettingsSection'; + +const ProfileEditPage: React.FC = () => { + const [activeSection, setActiveSection] = useState('profile'); + + return ( +
+
+

설정 메뉴

+
    +
  • setActiveSection('profile')} + > + 프로필 수정 +
  • +
  • setActiveSection('settings')} + > + 개인 설정 +
  • +
+
+
+ {activeSection === 'profile' && } + {activeSection === 'settings' && } +
+
+ ); +}; + +export default ProfileEditPage; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.module.css b/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.module.css new file mode 100644 index 00000000..e62d831e --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.module.css @@ -0,0 +1,53 @@ +.uploadContainer { + margin-bottom: 40px; +} + +.preview { + width: 100%; + height: 200px; + border: 1px dashed #ddd; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 15px; + background-color: #f9f9f9; +} + +.previewImage { + max-width: 100%; + max-height: 100%; + border-radius: 8px; +} + +.placeholder { + color: #aaa; +} + +.fileInputLabel { + display: block; + margin-bottom: 15px; + font-size: 16px; + color: #666; +} + +.fileInput { + display: none; /* 숨김 처리 */ +} + +.saveButton { + display: block; + width: 100%; + padding: 12px; + background-color: #007bff; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.saveButton:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.tsx b/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.tsx new file mode 100644 index 00000000..f2e5fbcd --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import styles from './ProfilePictureUpload.module.css'; + +const ProfilePictureUpload: React.FC<{ onSave: (file: File | null) => void }> = ({ onSave }) => { + const [selectedFile, setSelectedFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files ? event.target.files[0] : null; + setSelectedFile(file); + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setPreviewUrl(reader.result as string); + }; + reader.readAsDataURL(file); + } else { + setPreviewUrl(null); + } + }; + + const handleSave = () => { + if (selectedFile) { + onSave(selectedFile); + } + }; + + return ( +
+

프로필 사진 등록/수정

+
+ {previewUrl ? ( + 미리보기 + ) : ( +
미리보기 없음
+ )} +
+ + +
+ ); +}; + +export default ProfilePictureUpload; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfileSection.module.css b/frontend/src/pages/ProfileEditPage/ProfileSection.module.css new file mode 100644 index 00000000..7133f117 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfileSection.module.css @@ -0,0 +1,42 @@ +.profileSection { + margin-bottom: 40px; +} + +h2 { + font-size: 24px; + margin-bottom: 20px; + color: #333; +} + +label { + display: block; + margin-bottom: 15px; + font-size: 16px; + color: #666; +} + +.input { + width: 100%; + padding: 10px; + margin-top: 5px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 16px; +} + +.saveButton { + display: block; + width: 100%; + padding: 12px; + background-color: #007bff; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.saveButton:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfileSection.tsx b/frontend/src/pages/ProfileEditPage/ProfileSection.tsx new file mode 100644 index 00000000..701de607 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfileSection.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import styles from './ProfileSection.module.css'; +import ProfilePictureUpload from './ProfilePictureUpload'; + +const ProfileSection: React.FC = () => { + const [nickname, setNickname] = useState('사용자 닉네임'); + const [email, setEmail] = useState('user@example.com'); + const [password, setPassword] = useState(''); + const [profilePicture, setProfilePicture] = useState(null); + + const handleSaveProfile = () => { + // TODO: 프로필 저장 로직 추가 + console.log('프로필 저장:', { nickname, email, password, profilePicture }); + }; + + return ( +
+

프로필 수정

+ + + + + +
+ ); +}; + +export default ProfileSection; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/SettingsSection.module.css b/frontend/src/pages/ProfileEditPage/SettingsSection.module.css new file mode 100644 index 00000000..b6ddb712 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/SettingsSection.module.css @@ -0,0 +1,42 @@ +.settingsSection { + margin-bottom: 40px; +} + +h2 { + font-size: 24px; + margin-bottom: 20px; + color: #333; +} + +label { + display: block; + margin-bottom: 15px; + font-size: 16px; + color: #666; +} + +.select { + width: 100%; + padding: 10px; + margin-top: 5px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 16px; +} + +.saveButton { + display: block; + width: 100%; + padding: 12px; + background-color: #007bff; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.saveButton:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/SettingsSection.tsx b/frontend/src/pages/ProfileEditPage/SettingsSection.tsx new file mode 100644 index 00000000..a0b143d6 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/SettingsSection.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import styles from './SettingsSection.module.css'; + +const SettingsSection: React.FC = () => { + const [notificationsEnabled, setNotificationsEnabled] = useState(true); + const [profileVisibility, setProfileVisibility] = useState('public'); + const [adultVerification, setAdultVerification] = useState(false); + const [commentVisibility, setCommentVisibility] = useState('public'); + const [ratingVisibility, setRatingVisibility] = useState('public'); + const [webtoonPreferenceVisibility, setWebtoonPreferenceVisibility] = useState('public'); + const [recommendedWebtoonRange, setRecommendedWebtoonRange] = useState('all'); + const [hiddenUsers, setHiddenUsers] = useState([]); + const [blockedUsers, setBlockedUsers] = useState([]); + + const handleSaveSettings = () => { + // TODO: 설정 저장 로직 추가 + console.log('설정 저장:', { + notificationsEnabled, + profileVisibility, + adultVerification, + commentVisibility, + ratingVisibility, + webtoonPreferenceVisibility, + recommendedWebtoonRange, + hiddenUsers, + blockedUsers, + }); + }; + + return ( +
+

개인 설정

+ + + + + + + + +
+ ); +}; + +export default SettingsSection; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/index.ts b/frontend/src/pages/ProfileEditPage/index.ts new file mode 100644 index 00000000..24d9d731 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/index.ts @@ -0,0 +1 @@ +export { default } from './ProfileEditPage'; From 914c192de55bee6d05ba6e447dc19e7dabf9293c Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Sat, 25 Jan 2025 22:37:00 +0900 Subject: [PATCH 18/54] feat : add profile edit page --- frontend/src/App.tsx | 2 + .../MemberProfileSection.tsx | 1 + .../MemberProfileSection/index.ts | 2 +- .../ProfileEditPage.module.css | 95 +++++++++++++++ .../pages/ProfileEditPage/ProfileEditPage.tsx | 36 ++++++ .../ProfilePictureUpload.module.css | 53 +++++++++ .../ProfileEditPage/ProfilePictureUpload.tsx | 54 +++++++++ .../ProfileEditPage/ProfileSection.module.css | 42 +++++++ .../pages/ProfileEditPage/ProfileSection.tsx | 54 +++++++++ .../SettingsSection.module.css | 42 +++++++ .../pages/ProfileEditPage/SettingsSection.tsx | 111 ++++++++++++++++++ frontend/src/pages/ProfileEditPage/index.ts | 1 + 12 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/ProfileEditPage/ProfileEditPage.module.css create mode 100644 frontend/src/pages/ProfileEditPage/ProfileEditPage.tsx create mode 100644 frontend/src/pages/ProfileEditPage/ProfilePictureUpload.module.css create mode 100644 frontend/src/pages/ProfileEditPage/ProfilePictureUpload.tsx create mode 100644 frontend/src/pages/ProfileEditPage/ProfileSection.module.css create mode 100644 frontend/src/pages/ProfileEditPage/ProfileSection.tsx create mode 100644 frontend/src/pages/ProfileEditPage/SettingsSection.module.css create mode 100644 frontend/src/pages/ProfileEditPage/SettingsSection.tsx create mode 100644 frontend/src/pages/ProfileEditPage/index.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7d1bebf8..2e63e60f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,7 @@ import SocialLoginCallbackPage from '@pages/auth/SocialLoginCallbackPage'; import NewWebtoonsPage from '@pages/NewWebtoonsPage'; import OngoingWebtoonsPage from '@pages/OngoingWebtoonsPage'; import CompletedWebtoonsPage from '@pages/CompletedWebtoonsPage'; +import ProfileEditPage from '@pages/ProfileEditPage'; import ErrorPage from '@pages/ErrorPage/ErrorPage'; const App: React.FC = () => { @@ -33,6 +34,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx index 2be50a87..f277896a 100644 --- a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx +++ b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx @@ -26,6 +26,7 @@ const MemberProfileSection: React.FC = ({ memberProfi const handleEditProfile = () => { // TODO: 프로필 수정 기능 추가 + navigate('/profile/edit'); }; const handleMoreOptions = () => { diff --git a/frontend/src/pages/MyProfilePage/MemberProfileSection/index.ts b/frontend/src/pages/MyProfilePage/MemberProfileSection/index.ts index 90791067..87ed0f2a 100644 --- a/frontend/src/pages/MyProfilePage/MemberProfileSection/index.ts +++ b/frontend/src/pages/MyProfilePage/MemberProfileSection/index.ts @@ -1 +1 @@ -export { default } from './MemberProfileSection'; +export { default } from './MemberProfileSection'; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfileEditPage.module.css b/frontend/src/pages/ProfileEditPage/ProfileEditPage.module.css new file mode 100644 index 00000000..f0c425d7 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfileEditPage.module.css @@ -0,0 +1,95 @@ +.container { + display: flex; + max-width: 1200px; + margin: 0 auto; + padding: 20px; + background-color: #f9f9f9; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +} + +.sidebar { + flex: 1; + padding: 20px; + background-color: #ffffff; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + margin-right: 20px; +} + +.sidebar h2 { + font-size: 20px; + margin-bottom: 15px; + color: #333; +} + +.sidebar ul { + list-style: none; + padding: 0; +} + +.sidebar li { + padding: 10px; + cursor: pointer; + transition: background-color 0.3s; +} + +.sidebar li:hover { + background-color: #e0e0e0; +} + +.active { + background-color: #007bff; + color: white; +} + +.content { + flex: 3; + padding: 20px; + background-color: #ffffff; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +h2 { + font-size: 24px; + margin-bottom: 20px; + color: #333; +} + +.profileSection, .settingsSection { + margin-bottom: 40px; +} + +label { + display: block; + margin-bottom: 15px; + font-size: 16px; + color: #666; +} + +.input, .select { + width: 100%; + padding: 10px; + margin-top: 5px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 16px; +} + +.saveButton { + display: block; + width: 100%; + padding: 12px; + background-color: #007bff; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.saveButton:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfileEditPage.tsx b/frontend/src/pages/ProfileEditPage/ProfileEditPage.tsx new file mode 100644 index 00000000..a837cdee --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfileEditPage.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import styles from './ProfileEditPage.module.css'; +import ProfileSection from './ProfileSection'; +import SettingsSection from './SettingsSection'; + +const ProfileEditPage: React.FC = () => { + const [activeSection, setActiveSection] = useState('profile'); + + return ( +
+
+

설정 메뉴

+
    +
  • setActiveSection('profile')} + > + 프로필 수정 +
  • +
  • setActiveSection('settings')} + > + 개인 설정 +
  • +
+
+
+ {activeSection === 'profile' && } + {activeSection === 'settings' && } +
+
+ ); +}; + +export default ProfileEditPage; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.module.css b/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.module.css new file mode 100644 index 00000000..e62d831e --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.module.css @@ -0,0 +1,53 @@ +.uploadContainer { + margin-bottom: 40px; +} + +.preview { + width: 100%; + height: 200px; + border: 1px dashed #ddd; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 15px; + background-color: #f9f9f9; +} + +.previewImage { + max-width: 100%; + max-height: 100%; + border-radius: 8px; +} + +.placeholder { + color: #aaa; +} + +.fileInputLabel { + display: block; + margin-bottom: 15px; + font-size: 16px; + color: #666; +} + +.fileInput { + display: none; /* 숨김 처리 */ +} + +.saveButton { + display: block; + width: 100%; + padding: 12px; + background-color: #007bff; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.saveButton:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.tsx b/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.tsx new file mode 100644 index 00000000..f2e5fbcd --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import styles from './ProfilePictureUpload.module.css'; + +const ProfilePictureUpload: React.FC<{ onSave: (file: File | null) => void }> = ({ onSave }) => { + const [selectedFile, setSelectedFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files ? event.target.files[0] : null; + setSelectedFile(file); + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setPreviewUrl(reader.result as string); + }; + reader.readAsDataURL(file); + } else { + setPreviewUrl(null); + } + }; + + const handleSave = () => { + if (selectedFile) { + onSave(selectedFile); + } + }; + + return ( +
+

프로필 사진 등록/수정

+
+ {previewUrl ? ( + 미리보기 + ) : ( +
미리보기 없음
+ )} +
+ + +
+ ); +}; + +export default ProfilePictureUpload; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfileSection.module.css b/frontend/src/pages/ProfileEditPage/ProfileSection.module.css new file mode 100644 index 00000000..7133f117 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfileSection.module.css @@ -0,0 +1,42 @@ +.profileSection { + margin-bottom: 40px; +} + +h2 { + font-size: 24px; + margin-bottom: 20px; + color: #333; +} + +label { + display: block; + margin-bottom: 15px; + font-size: 16px; + color: #666; +} + +.input { + width: 100%; + padding: 10px; + margin-top: 5px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 16px; +} + +.saveButton { + display: block; + width: 100%; + padding: 12px; + background-color: #007bff; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.saveButton:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfileSection.tsx b/frontend/src/pages/ProfileEditPage/ProfileSection.tsx new file mode 100644 index 00000000..701de607 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfileSection.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import styles from './ProfileSection.module.css'; +import ProfilePictureUpload from './ProfilePictureUpload'; + +const ProfileSection: React.FC = () => { + const [nickname, setNickname] = useState('사용자 닉네임'); + const [email, setEmail] = useState('user@example.com'); + const [password, setPassword] = useState(''); + const [profilePicture, setProfilePicture] = useState(null); + + const handleSaveProfile = () => { + // TODO: 프로필 저장 로직 추가 + console.log('프로필 저장:', { nickname, email, password, profilePicture }); + }; + + return ( +
+

프로필 수정

+ + + + + +
+ ); +}; + +export default ProfileSection; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/SettingsSection.module.css b/frontend/src/pages/ProfileEditPage/SettingsSection.module.css new file mode 100644 index 00000000..b6ddb712 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/SettingsSection.module.css @@ -0,0 +1,42 @@ +.settingsSection { + margin-bottom: 40px; +} + +h2 { + font-size: 24px; + margin-bottom: 20px; + color: #333; +} + +label { + display: block; + margin-bottom: 15px; + font-size: 16px; + color: #666; +} + +.select { + width: 100%; + padding: 10px; + margin-top: 5px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 16px; +} + +.saveButton { + display: block; + width: 100%; + padding: 12px; + background-color: #007bff; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.saveButton:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/SettingsSection.tsx b/frontend/src/pages/ProfileEditPage/SettingsSection.tsx new file mode 100644 index 00000000..a0b143d6 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/SettingsSection.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import styles from './SettingsSection.module.css'; + +const SettingsSection: React.FC = () => { + const [notificationsEnabled, setNotificationsEnabled] = useState(true); + const [profileVisibility, setProfileVisibility] = useState('public'); + const [adultVerification, setAdultVerification] = useState(false); + const [commentVisibility, setCommentVisibility] = useState('public'); + const [ratingVisibility, setRatingVisibility] = useState('public'); + const [webtoonPreferenceVisibility, setWebtoonPreferenceVisibility] = useState('public'); + const [recommendedWebtoonRange, setRecommendedWebtoonRange] = useState('all'); + const [hiddenUsers, setHiddenUsers] = useState([]); + const [blockedUsers, setBlockedUsers] = useState([]); + + const handleSaveSettings = () => { + // TODO: 설정 저장 로직 추가 + console.log('설정 저장:', { + notificationsEnabled, + profileVisibility, + adultVerification, + commentVisibility, + ratingVisibility, + webtoonPreferenceVisibility, + recommendedWebtoonRange, + hiddenUsers, + blockedUsers, + }); + }; + + return ( +
+

개인 설정

+ + + + + + + + +
+ ); +}; + +export default SettingsSection; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/index.ts b/frontend/src/pages/ProfileEditPage/index.ts new file mode 100644 index 00000000..f8054ca3 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/index.ts @@ -0,0 +1 @@ +export { default } from './ProfileEditPage'; \ No newline at end of file From e5a7dabbf8089ed87319cf649006b11b31b8699a Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:52:10 +0900 Subject: [PATCH 19/54] feat: add build file --- backend/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/build.gradle b/backend/build.gradle index a19cbef5..8a70be0d 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -7,6 +7,10 @@ plugins { group = 'toonpick' version = '0.0.1-SNAPSHOT' +bootJar { + archiveFileName = "toonpick-${version}.jar" +} + java { sourceCompatibility = '17' } From 6a7a1dbc3bd0fa3add337584b587abb3c7f68847 Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:56:42 +0900 Subject: [PATCH 20/54] feat: add log file path --- backend/src/main/resources/application.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 5fe7b531..af384864 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -87,4 +87,7 @@ logging: springframework: DEBUG hibernate: SQL: DEBUG + file: + name: logs/application.log + max-size: 10MB From 8bc5375500eade911961455de6cd3e6f9c34cac4 Mon Sep 17 00:00:00 2001 From: ImGdevel Date: Tue, 28 Jan 2025 17:28:01 +0900 Subject: [PATCH 21/54] setup: setup docker build --- backend/.dockerignore | 7 +++++++ backend/Dockerfile | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..97f7bd6c --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,7 @@ +.gradle +/build +/src/test +*.jar +*.log +.DS_Store + diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..1a053e7b --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,17 @@ +FROM eclipse-temurin:17-jdk as builder + +WORKDIR /app + +COPY . /app + +RUN chmod +x ./gradlew + +RUN ./gradlew clean bootJar + +FROM eclipse-temurin:17-jre +WORKDIR /app + +COPY --from=builder /app/build/libs/*.jar app.jar + +ENTRYPOINT ["java", "-jar", "/app/app.jar"] + From 121c8810086f05088cf5ce3f4f75f19c76f21731 Mon Sep 17 00:00:00 2001 From: ImGdevel Date: Tue, 28 Jan 2025 17:51:10 +0900 Subject: [PATCH 22/54] feat: add docker-compose yml --- backend/docker-compose.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 backend/docker-compose.yml diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 00000000..126b1bc7 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.9" + +services: + spring-app: + image: toonpick-service-app:0.0.1 + container_name: toonpick-service-app + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" + volumes: + - ./logs:/app/logs + + environment: + LOG_FILE: /app/logs/application.log + + restart: always + From 102fb4066ce01666a84ced8c0deecded3a0c0c80 Mon Sep 17 00:00:00 2001 From: ImGdevel Date: Sun, 2 Feb 2025 17:33:35 +0900 Subject: [PATCH 23/54] feat: docker config --- backend/docker-compose.yml | 76 +++++++++++++++++++-- backend/logs/info_2024-12-27.gz | Bin 16373 -> 0 bytes backend/logs/info_2025-01-03.gz | Bin 14033 -> 0 bytes backend/logs/info_2025-01-28.gz | Bin 0 -> 12473 bytes backend/src/main/resources/application.yml | 6 +- 5 files changed, 74 insertions(+), 8 deletions(-) delete mode 100644 backend/logs/info_2024-12-27.gz delete mode 100644 backend/logs/info_2025-01-03.gz create mode 100644 backend/logs/info_2025-01-28.gz diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 126b1bc7..0fabdbbf 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -1,19 +1,85 @@ -version: "3.9" - services: spring-app: image: toonpick-service-app:0.0.1 container_name: toonpick-service-app - build: + build: context: . dockerfile: Dockerfile ports: - "8080:8080" volumes: - ./logs:/app/logs - environment: LOG_FILE: /app/logs/application.log - + SPRING_DATASOURCE_META_URL: jdbc:mariadb://toonpick-db-meta:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + SPRING_DATASOURCE_META_USERNAME: root + SPRING_DATASOURCE_META_PASSWORD: 1234 + SPRING_DATASOURCE_DATA_URL: jdbc:mariadb://toonpick-db:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + SPRING_DATASOURCE_DATA_USERNAME: root + SPRING_DATASOURCE_DATA_PASSWORD: 1234 restart: always + depends_on: + mariadb: + condition: service_healthy + mariadb-meta: + condition: service_healthy + redis: + condition: service_started + networks: + - backend + + mariadb: + image: mariadb:10.5 + container_name: toonpick-db + environment: + MYSQL_ROOT_PASSWORD: 1234 + MYSQL_DATABASE: toonpick-database + ports: + - "3306:3306" + volumes: + - mariadb-data:/var/lib/mysql + networks: + - backend + healthcheck: + test: ["CMD", "mysqladmin", "ping", "--host=localhost", "--user=root", "--password=1234"] + interval: 30s + retries: 5 + timeout: 10s + start_period: 30s + + mariadb-meta: + image: mariadb:10.5 + container_name: toonpick-db-meta + environment: + MYSQL_ROOT_PASSWORD: 1234 + MYSQL_DATABASE: meta + ports: + - "3307:3306" + volumes: + - mariadb-meta-data:/var/lib/mysql + networks: + - backend + healthcheck: + test: ["CMD", "mysqladmin", "ping", "--host=localhost", "--user=root", "--password=1234"] + interval: 30s + retries: 5 + timeout: 10s + start_period: 30s + + redis: + image: redis:6.0 + container_name: toonpick-redis + ports: + - "6380:6379" + networks: + - backend + +volumes: + mariadb-data: + driver: local + mariadb-meta-data: + driver: local +networks: + backend: + driver: bridge diff --git a/backend/logs/info_2024-12-27.gz b/backend/logs/info_2024-12-27.gz deleted file mode 100644 index d89fc6c9cfead057198da87b4dc5d58be39bc337..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16373 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 diff --git a/backend/logs/info_2025-01-28.gz b/backend/logs/info_2025-01-28.gz new file mode 100644 index 0000000000000000000000000000000000000000..7e181617e2fdfd1693b47e8b75a84f32d71226e4 GIT binary patch literal 12473 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 literal 0 HcmV?d00001 diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index af384864..a943a035 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -7,18 +7,18 @@ spring: datasource-meta: driver-class-name: org.mariadb.jdbc.Driver - jdbc-url: jdbc:mariadb://localhost:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + jdbc-url: jdbc:mariadb://mariadb:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true username: root password: 1234 datasource-data: driver-class-name: org.mariadb.jdbc.Driver - jdbc-url: jdbc:mariadb://localhost:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + jdbc-url: jdbc:mariadb://mariadb:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true username: root password: 1234 redis: - host: localhost + host: redis port: 6380 timeout: 10000ms jedis: From 997ada9ea70d5c23b9110e47b47da9e93a641493 Mon Sep 17 00:00:00 2001 From: ImGdevel Date: Sun, 2 Feb 2025 17:42:21 +0900 Subject: [PATCH 24/54] fix: fix db url --- backend/src/main/resources/application.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index a943a035..693842b3 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -7,13 +7,13 @@ spring: datasource-meta: driver-class-name: org.mariadb.jdbc.Driver - jdbc-url: jdbc:mariadb://mariadb:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + jdbc-url: jdbc:mariadb://toonpick-db-meta:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true username: root password: 1234 datasource-data: driver-class-name: org.mariadb.jdbc.Driver - jdbc-url: jdbc:mariadb://mariadb:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + jdbc-url: jdbc:mariadb://toonpick-db:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true username: root password: 1234 From b511bf8097655e3ccfd6221863323623fac62d4c Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Sun, 2 Feb 2025 17:48:06 +0900 Subject: [PATCH 25/54] feat: add ping test controller --- .../app/controller/HelloController.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 backend/src/main/java/toonpick/app/controller/HelloController.java diff --git a/backend/src/main/java/toonpick/app/controller/HelloController.java b/backend/src/main/java/toonpick/app/controller/HelloController.java new file mode 100644 index 00000000..fb9e92b8 --- /dev/null +++ b/backend/src/main/java/toonpick/app/controller/HelloController.java @@ -0,0 +1,31 @@ +package toonpick.app.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +@Controller +public class HelloController { + + final private Logger logger = LoggerFactory.getLogger(HelloController.class); + + @GetMapping("/api/public/ping") + private ResponseEntity> testPing() { + Map response = new HashMap<>(); + + response.put("status", "success"); + response.put("message", "TOONPICK 서비스가 정상적으로 작동 중입니다."); + response.put("timestamp", LocalDateTime.now()); + + logger.info("Ping 요청이 들어왔습니다. 응답: {}", response); + + return ResponseEntity.status(HttpStatus.OK).body(response); + } +} From 6be44370779cba522cee9845ae126d78dfdd44ff Mon Sep 17 00:00:00 2001 From: ImGdevel Date: Sun, 2 Feb 2025 18:36:08 +0900 Subject: [PATCH 26/54] feat: db congif env --- backend/docker-compose.yml | 6 ++--- .../toonpick/app/config/DataDBConfig.java | 2 +- .../toonpick/app/config/MetaDBConfig.java | 2 +- backend/src/main/resources/application.yml | 22 +++++++++---------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 0fabdbbf..98490227 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -11,12 +11,12 @@ services: - ./logs:/app/logs environment: LOG_FILE: /app/logs/application.log - SPRING_DATASOURCE_META_URL: jdbc:mariadb://toonpick-db-meta:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true - SPRING_DATASOURCE_META_USERNAME: root - SPRING_DATASOURCE_META_PASSWORD: 1234 SPRING_DATASOURCE_DATA_URL: jdbc:mariadb://toonpick-db:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true SPRING_DATASOURCE_DATA_USERNAME: root SPRING_DATASOURCE_DATA_PASSWORD: 1234 + SPRING_DATASOURCE_META_URL: jdbc:mariadb://toonpick-db-meta:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + SPRING_DATASOURCE_META_USERNAME: root + SPRING_DATASOURCE_META_PASSWORD: 1234 restart: always depends_on: mariadb: diff --git a/backend/src/main/java/toonpick/app/config/DataDBConfig.java b/backend/src/main/java/toonpick/app/config/DataDBConfig.java index 79c66dde..0f122fa7 100644 --- a/backend/src/main/java/toonpick/app/config/DataDBConfig.java +++ b/backend/src/main/java/toonpick/app/config/DataDBConfig.java @@ -23,7 +23,7 @@ public class DataDBConfig { @Bean - @ConfigurationProperties(prefix = "spring.datasource-data") + @ConfigurationProperties(prefix = "spring.datasource.data") public DataSource dataDBSource() { return DataSourceBuilder.create().build(); } diff --git a/backend/src/main/java/toonpick/app/config/MetaDBConfig.java b/backend/src/main/java/toonpick/app/config/MetaDBConfig.java index 3caeac82..958a250f 100644 --- a/backend/src/main/java/toonpick/app/config/MetaDBConfig.java +++ b/backend/src/main/java/toonpick/app/config/MetaDBConfig.java @@ -16,7 +16,7 @@ public class MetaDBConfig { @Primary @Bean - @ConfigurationProperties(prefix = "spring.datasource-meta") + @ConfigurationProperties(prefix = "spring.datasource.meta") public DataSource metaDBSource(){ return DataSourceBuilder.create().build(); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 693842b3..99843780 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -5,17 +5,17 @@ spring: config: import: application-API-KEY.yml - datasource-meta: - driver-class-name: org.mariadb.jdbc.Driver - jdbc-url: jdbc:mariadb://toonpick-db-meta:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true - username: root - password: 1234 - - datasource-data: - driver-class-name: org.mariadb.jdbc.Driver - jdbc-url: jdbc:mariadb://toonpick-db:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true - username: root - password: 1234 + datasource: + data: + driver-class-name: org.mariadb.jdbc.Driver + jdbc-url: ${SPRING_DATASOURCE_DATA_URL} + username: ${SPRING_DATASOURCE_DATA_USERNAME} + password: ${SPRING_DATASOURCE_DATA_PASSWORD} + meta: + driver-class-name: org.mariadb.jdbc.Driver + jdbc-url: ${SPRING_DATASOURCE_META_URL} + username: ${SPRING_DATASOURCE_META_USERNAME} + password: ${SPRING_DATASOURCE_META_PASSWORD} redis: host: redis From 9a3e409bb9b0c47f86274a09cd137c197f72db39 Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:52:10 +0900 Subject: [PATCH 27/54] feat: add build file --- backend/build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/backend/build.gradle b/backend/build.gradle index a19cbef5..8a70be0d 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -7,6 +7,10 @@ plugins { group = 'toonpick' version = '0.0.1-SNAPSHOT' +bootJar { + archiveFileName = "toonpick-${version}.jar" +} + java { sourceCompatibility = '17' } From cf3d9c987695db9bc8993eb9b8a20ef021217011 Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:56:42 +0900 Subject: [PATCH 28/54] feat: add log file path --- backend/src/main/resources/application.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 5fe7b531..af384864 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -87,4 +87,7 @@ logging: springframework: DEBUG hibernate: SQL: DEBUG + file: + name: logs/application.log + max-size: 10MB From c19682119f510ac2ef87fe149fd0b0763d906f23 Mon Sep 17 00:00:00 2001 From: ImGdevel Date: Tue, 28 Jan 2025 17:28:01 +0900 Subject: [PATCH 29/54] setup: setup docker build --- backend/.dockerignore | 7 +++++++ backend/Dockerfile | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 backend/.dockerignore create mode 100644 backend/Dockerfile diff --git a/backend/.dockerignore b/backend/.dockerignore new file mode 100644 index 00000000..97f7bd6c --- /dev/null +++ b/backend/.dockerignore @@ -0,0 +1,7 @@ +.gradle +/build +/src/test +*.jar +*.log +.DS_Store + diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..1a053e7b --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,17 @@ +FROM eclipse-temurin:17-jdk as builder + +WORKDIR /app + +COPY . /app + +RUN chmod +x ./gradlew + +RUN ./gradlew clean bootJar + +FROM eclipse-temurin:17-jre +WORKDIR /app + +COPY --from=builder /app/build/libs/*.jar app.jar + +ENTRYPOINT ["java", "-jar", "/app/app.jar"] + From 08b4918aeb25c58bef236e6e614d030662c4b4c3 Mon Sep 17 00:00:00 2001 From: ImGdevel Date: Tue, 28 Jan 2025 17:51:10 +0900 Subject: [PATCH 30/54] feat: add docker-compose yml --- backend/docker-compose.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 backend/docker-compose.yml diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 00000000..126b1bc7 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,19 @@ +version: "3.9" + +services: + spring-app: + image: toonpick-service-app:0.0.1 + container_name: toonpick-service-app + build: + context: . + dockerfile: Dockerfile + ports: + - "8080:8080" + volumes: + - ./logs:/app/logs + + environment: + LOG_FILE: /app/logs/application.log + + restart: always + From 2750fe7bb5594f62c02647c444d47aa9760e50c9 Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Tue, 14 Jan 2025 20:53:43 +0900 Subject: [PATCH 31/54] feat : slide card --- .../WebtoonCard/WebtoonCard.module.css | 2 -- .../components/WebtoonCard/WebtoonCard.tsx | 26 +++++++++++++--- .../WebtoonList/WebtoonList.module.css | 10 +++--- .../components/WebtoonList/WebtoonList.tsx | 31 ++++++++++--------- 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/WebtoonCard/WebtoonCard.module.css b/frontend/src/components/WebtoonCard/WebtoonCard.module.css index e77d7960..cecf4f4e 100644 --- a/frontend/src/components/WebtoonCard/WebtoonCard.module.css +++ b/frontend/src/components/WebtoonCard/WebtoonCard.module.css @@ -1,6 +1,4 @@ .webtoonCard { - width: 220px; - height: 360px; display: flex; flex-direction: column; align-items: center; diff --git a/frontend/src/components/WebtoonCard/WebtoonCard.tsx b/frontend/src/components/WebtoonCard/WebtoonCard.tsx index c4c6043d..08cc2747 100644 --- a/frontend/src/components/WebtoonCard/WebtoonCard.tsx +++ b/frontend/src/components/WebtoonCard/WebtoonCard.tsx @@ -7,15 +7,31 @@ import PlatformIcon from '../PlatformIcon/PlatformIcon'; interface WebtoonCardProps { webtoon: Webtoon; showTags?: boolean; + size?: number; } -const WebtoonCard: React.FC = ({ webtoon, showTags = true }) => { - const authors = webtoon.authors?.map(author => author.name).join(', ') || '작가 없음'; - const averageRating = webtoon.averageRating ? webtoon.averageRating.toFixed(1) : '0'; - const truncatedTitle = webtoon.title.length > 30 ? `${webtoon.title.substring(0, 30)}...` : webtoon.title; +const WebtoonCard: React.FC = ({ webtoon, showTags = true, size = 360 }) => { + const getAuthors = (authors: { name: string }[] | undefined): string => { + return authors?.map(author => author.name).join(', ') || '작가 없음'; + }; + + const formatAverageRating = (rating: number | undefined): string => { + return rating ? rating.toFixed(1) : '0'; + }; + + const truncateTitle = (title: string): string => { + return title.length > 30 ? `${title.substring(0, 30)}...` : title; + }; + + const authors = getAuthors(webtoon.authors); + const averageRating = formatAverageRating(webtoon.averageRating); + const truncatedTitle = truncateTitle(webtoon.title); + + const height = size; + const width = (size * 220) / 360; return ( - +
{webtoon.title} diff --git a/frontend/src/components/WebtoonList/WebtoonList.module.css b/frontend/src/components/WebtoonList/WebtoonList.module.css index 561f4a0d..8709f962 100644 --- a/frontend/src/components/WebtoonList/WebtoonList.module.css +++ b/frontend/src/components/WebtoonList/WebtoonList.module.css @@ -1,15 +1,17 @@ .webtoonList { - padding: 20px; + padding: 10px; + position: relative; + overflow: hidden; } .carousel { display: flex; - overflow: hidden; - scroll-behavior: smooth; + transition: transform 0.5s ease; + will-change: transform; } .carousel > * { - flex: 0 0 20%; + flex: 0 0 auto; margin-right: 20px; } diff --git a/frontend/src/components/WebtoonList/WebtoonList.tsx b/frontend/src/components/WebtoonList/WebtoonList.tsx index c12e6947..117874b8 100644 --- a/frontend/src/components/WebtoonList/WebtoonList.tsx +++ b/frontend/src/components/WebtoonList/WebtoonList.tsx @@ -10,7 +10,7 @@ interface WebtoonListProps { const WebtoonList: React.FC = ({ webtoons, showTags = true }) => { const [currentPage, setCurrentPage] = useState(0); - const itemsPerPage = 5; + const itemsPerPage = 4; const totalPages = Math.ceil(webtoons.length / itemsPerPage); const handleNext = () => { @@ -28,21 +28,24 @@ const WebtoonList: React.FC = ({ webtoons, showTags = true }) const startIndex = currentPage * itemsPerPage; const currentWebtoons = webtoons.slice(startIndex, startIndex + itemsPerPage); + const cardHeight = 360; + const cardWidth = ((cardHeight * 220) / 360 + 20) * 4; + return (
- {currentWebtoons.length > 0 ? ( -
- {currentWebtoons.map((webtoon) => ( - - ))} -
- ) : ( -

웹툰이 없습니다.

- )} +
+ {webtoons.map((webtoon) => ( + + ))} +
void; onLogout: () => void; isWidgetOpen: boolean; @@ -14,9 +13,7 @@ interface ProfileWidgetProps { } const ProfileWidget: React.FC = ({ - userProfilePic, - userName, - userEmail, + memberProfile, onNavigate, onLogout, isWidgetOpen, @@ -45,6 +42,8 @@ const ProfileWidget: React.FC = ({ }; }, [handleClickOutside]); + const defaultProfilePicture = '/image/profile/user.png'; + return (
= ({ style={{ pointerEvents: isWidgetOpen ? 'auto' : 'none' }} ref={profileWidgetRef} > - User Profile + User Profile
-

{userName}

-

{userEmail}

+

{memberProfile?.nickname || '게스트 사용자'}

@@ -85,7 +85,7 @@ const MyProfilePage: React.FC = () => {

좋아요

From ef228441a22ddb389b403f2f27b6f65d44ec8af0 Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:39:41 +0900 Subject: [PATCH 43/54] fix : authorition error --- .../app/security/handler/OAuth2SuccessHandler.java | 7 ++----- .../java/toonpick/app/security/jwt/JwtTokenValidator.java | 8 +++----- frontend/src/pages/auth/SignUpPage/SignUpPage.tsx | 2 ++ frontend/src/services/AuthService.ts | 5 +---- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/backend/src/main/java/toonpick/app/security/handler/OAuth2SuccessHandler.java b/backend/src/main/java/toonpick/app/security/handler/OAuth2SuccessHandler.java index 6d52464d..e1b371e4 100644 --- a/backend/src/main/java/toonpick/app/security/handler/OAuth2SuccessHandler.java +++ b/backend/src/main/java/toonpick/app/security/handler/OAuth2SuccessHandler.java @@ -32,11 +32,8 @@ public void onAuthenticationSuccess(HttpServletRequest request, HttpServletRespo String role = authentication.getAuthorities().stream(). findFirst().map(GrantedAuthority::getAuthority).orElse(""); - String refreshToken = tokenService.issueAccessToken(username, role); - String accessToken = tokenService.issueRefreshToken(username, role); - - response.setHeader("Authorization", "Bearer " + accessToken); - response.addCookie(CookieUtils.createEmptyCookie(refreshToken)); + String refreshToken = tokenService.issueRefreshToken(username, role); + response.addCookie(CookieUtils.createRefreshCookie(refreshToken)); response.setStatus(HttpStatus.OK.value()); logger.info("USER LOGIN SUCCESS (username-{})", username); diff --git a/backend/src/main/java/toonpick/app/security/jwt/JwtTokenValidator.java b/backend/src/main/java/toonpick/app/security/jwt/JwtTokenValidator.java index 579ee124..892c4fc7 100644 --- a/backend/src/main/java/toonpick/app/security/jwt/JwtTokenValidator.java +++ b/backend/src/main/java/toonpick/app/security/jwt/JwtTokenValidator.java @@ -54,14 +54,12 @@ public void validateRefreshToken(String refreshToken) { if (refreshToken == null || refreshToken.isEmpty()) { throw new MissingJwtTokenException(ErrorCode.REFRESH_TOKEN_MISSING); } - + if (!"refresh".equals(jwtTokenProvider.getCategory(refreshToken))) { + throw new InvalidJwtTokenException(ErrorCode.REFRESH_TOKEN_INVALID, jwtTokenProvider.getCategory(refreshToken)); + } if (jwtTokenProvider.isExpired(refreshToken)) { throw new ExpiredJwtTokenException(ErrorCode.EXPIRED_REFRESH_TOKEN); } - - if (!"refresh".equals(jwtTokenProvider.getCategory(refreshToken))) { - throw new InvalidJwtTokenException(ErrorCode.REFRESH_TOKEN_INVALID); - } } // 유저 정보 추출 diff --git a/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx b/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx index dd200df7..9423d868 100644 --- a/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx +++ b/frontend/src/pages/auth/SignUpPage/SignUpPage.tsx @@ -64,6 +64,8 @@ const SignUpPage: React.FC = () => { if (response.success) { navigate('/login'); + }else{ + setError('회원가입에 실패했습니다. 다시 시도해주세요.' + response.message); } } catch (err) { setError('회원가입에 실패했습니다. 다시 시도해주세요.'); diff --git a/frontend/src/services/AuthService.ts b/frontend/src/services/AuthService.ts index ac5b6c11..b6d51f33 100644 --- a/frontend/src/services/AuthService.ts +++ b/frontend/src/services/AuthService.ts @@ -25,10 +25,7 @@ export const AuthService = { }, // 회원가입 - signup: async (username: string, password: string, confirmPassword: string): Promise => { - if (password !== confirmPassword) { - return { success: false, message: 'Passwords do not match.' }; - } + signup: async (username: string, email: string ,password: string): Promise => { try { const joinPayload = { username, From a873475655f93019d441cb96943f409dd850c1b1 Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:54:53 +0900 Subject: [PATCH 44/54] feat: failed authorticated error This reverts commit 9207b2d2cd3fae6a95f58a5b95cdd6eb10b69ae4. --- .../toonpick/app/exception/ErrorCode.java | 4 ++++ .../CustomAuthenticationException.java | 19 +++++++++++++++ .../AuthenticationExceptionHandler.java | 24 ++++++++++--------- .../filter/LoginAuthenticationFilter.java | 17 +++++++++++-- 4 files changed, 51 insertions(+), 13 deletions(-) create mode 100644 backend/src/main/java/toonpick/app/exception/exception/CustomAuthenticationException.java diff --git a/backend/src/main/java/toonpick/app/exception/ErrorCode.java b/backend/src/main/java/toonpick/app/exception/ErrorCode.java index d9590f9e..9e738b6a 100644 --- a/backend/src/main/java/toonpick/app/exception/ErrorCode.java +++ b/backend/src/main/java/toonpick/app/exception/ErrorCode.java @@ -18,6 +18,10 @@ public enum ErrorCode { PERMISSION_DENIED(1002, "Permission denied"), // 2xxx : Security error + AUTHENTICATION_FAILED(2000, "Authentication failed"), + INVALID_CREDENTIALS(2001, "Invalid username or password"), + INVALID_JSON_FORMAT(2002, "Invalid JSON format in request"), + REQUEST_BODY_READ_ERROR(2003, "Failed to read authentication request body"), // 21xx : authorization error USER_ALREADY_REGISTERED(2103, "User is already registered"), diff --git a/backend/src/main/java/toonpick/app/exception/exception/CustomAuthenticationException.java b/backend/src/main/java/toonpick/app/exception/exception/CustomAuthenticationException.java new file mode 100644 index 00000000..0544a8e3 --- /dev/null +++ b/backend/src/main/java/toonpick/app/exception/exception/CustomAuthenticationException.java @@ -0,0 +1,19 @@ +package toonpick.app.exception.exception; + +import lombok.Getter; +import toonpick.app.exception.ErrorCode; + +@Getter +public class CustomAuthenticationException extends RuntimeException { + private final ErrorCode errorCode; + + public CustomAuthenticationException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public CustomAuthenticationException(ErrorCode errorCode, Throwable cause) { + super(errorCode.getMessage(), cause); + this.errorCode = errorCode; + } +} diff --git a/backend/src/main/java/toonpick/app/exception/handler/AuthenticationExceptionHandler.java b/backend/src/main/java/toonpick/app/exception/handler/AuthenticationExceptionHandler.java index 2a5f74c1..1b02718e 100644 --- a/backend/src/main/java/toonpick/app/exception/handler/AuthenticationExceptionHandler.java +++ b/backend/src/main/java/toonpick/app/exception/handler/AuthenticationExceptionHandler.java @@ -9,6 +9,7 @@ import org.springframework.security.web.csrf.InvalidCsrfTokenException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import toonpick.app.exception.exception.CustomAuthenticationException; import toonpick.app.exception.exception.UserAlreadyRegisteredException; import java.nio.file.AccessDeniedException; @@ -21,43 +22,44 @@ public class AuthenticationExceptionHandler { @ExceptionHandler(UsernameNotFoundException.class) public ResponseEntity handleUsernameNotFoundException(UsernameNotFoundException ex) { LOGGER.error("Username not found: {}", ex.getMessage()); - return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Username not found: " + ex.getMessage()); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage()); } @ExceptionHandler(AccessDeniedException.class) public ResponseEntity handleAccessDeniedException(AccessDeniedException ex) { LOGGER.error("Access denied: {}", ex.getMessage()); - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Access is denied. " + ex.getMessage()); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ex.getMessage()); + } + + @ExceptionHandler(CustomAuthenticationException.class) + public ResponseEntity handleAuthenticationException(CustomAuthenticationException ex) { + LOGGER.error("Authentication Failed: {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getMessage()); } @ExceptionHandler(AuthenticationCredentialsNotFoundException.class) public ResponseEntity handleAuthenticationCredentialsNotFoundException( AuthenticationCredentialsNotFoundException ex) { LOGGER.error("Authentication credentials not found: {}", ex.getMessage()); - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Authentication credentials are required."); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ex.getMessage()); } @ExceptionHandler(UserAlreadyRegisteredException.class) public ResponseEntity handleUsernameAlreadyExistsException(UserAlreadyRegisteredException ex) { LOGGER.error("Username already exists: {}", ex.getMessage()); - return ResponseEntity.status(HttpStatus.CONFLICT).body("Username already exists: " + ex.getMessage()); + return ResponseEntity.status(HttpStatus.CONFLICT).body(ex.getMessage()); } @ExceptionHandler(InvalidCsrfTokenException.class) public ResponseEntity handleInvalidTokenException(InvalidCsrfTokenException ex) { LOGGER.error("Invalid Token: {}", ex.getMessage()); - return ResponseEntity.status(HttpStatus.FORBIDDEN).body("Invalid CSRF token: " + ex.getMessage()); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ex.getMessage()); } @ExceptionHandler(NullPointerException.class) public ResponseEntity handleNullPointerException(NullPointerException ex) { LOGGER.error("Null pointer exception: {}", ex.getMessage(), ex); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("Invalid request: " + ex.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage()); } - @ExceptionHandler(Exception.class) - public ResponseEntity handleGeneralException(Exception ex) { - LOGGER.error("Unexpected error in authentication: {}", ex.getMessage(), ex); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An unexpected error occurred: " + ex.getMessage()); - } } diff --git a/backend/src/main/java/toonpick/app/security/filter/LoginAuthenticationFilter.java b/backend/src/main/java/toonpick/app/security/filter/LoginAuthenticationFilter.java index 76b78ca3..208a9a45 100644 --- a/backend/src/main/java/toonpick/app/security/filter/LoginAuthenticationFilter.java +++ b/backend/src/main/java/toonpick/app/security/filter/LoginAuthenticationFilter.java @@ -1,5 +1,6 @@ package toonpick.app.security.filter; +import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.FilterChain; import jakarta.servlet.http.HttpServletRequest; @@ -10,11 +11,14 @@ import org.slf4j.LoggerFactory; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.AuthenticationServiceException; +import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.core.AuthenticationException; +import toonpick.app.exception.ErrorCode; +import toonpick.app.exception.exception.CustomAuthenticationException; import toonpick.app.security.dto.LoginRequest; import toonpick.app.security.handler.LoginFailureHandler; import toonpick.app.security.handler.LoginSuccessHandler; @@ -44,9 +48,18 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password); return authenticationManager.authenticate(authenticationToken); + } catch (BadCredentialsException e) { + logger.error(ErrorCode.INVALID_CREDENTIALS.getMessage()); + throw new CustomAuthenticationException(ErrorCode.INVALID_CREDENTIALS); + } catch (JsonProcessingException e) { + logger.error(ErrorCode.INVALID_JSON_FORMAT.getMessage()); + throw new CustomAuthenticationException(ErrorCode.INVALID_JSON_FORMAT); + } catch (IOException e) { + logger.error(ErrorCode.REQUEST_BODY_READ_ERROR.getMessage()); + throw new CustomAuthenticationException(ErrorCode.REQUEST_BODY_READ_ERROR); } catch (Exception e) { - logger.error("Failed to parse authentication request body", e); - throw new AuthenticationServiceException("Failed to parse authentication request body", e); + logger.error(ErrorCode.UNKNOWN_ERROR.getMessage(), e); + throw new CustomAuthenticationException(ErrorCode.UNKNOWN_ERROR, e); } } From b844fc56f4dbf4e856528a8fb5312fb18fdfd63c Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Sat, 25 Jan 2025 21:21:15 +0900 Subject: [PATCH 45/54] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20?= =?UTF-8?q?=EB=B0=98=EC=9D=91=EC=84=B1=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 소셜 로그인 시 form에서 필요 요건 메시지 가 뜨는 문제점 해결 - 프로필 위젯이 제대로 닫기지 않는 문제점 해결 --- frontend/src/components/Header/Header.tsx | 93 +++---------------- frontend/src/components/Header/Menu.tsx | 20 ++++ frontend/src/components/Header/Search.tsx | 54 +++++++++++ .../ProfileWidget/ProfileWidget.tsx | 17 ++-- .../SocialLoginButton/SocialLoginButton.tsx | 5 +- .../src/pages/MyProfilePage/MyProfilePage.tsx | 2 - .../src/pages/auth/SignInPage/SignInPage.tsx | 14 ++- 7 files changed, 111 insertions(+), 94 deletions(-) create mode 100644 frontend/src/components/Header/Menu.tsx create mode 100644 frontend/src/components/Header/Search.tsx diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx index 311f98d4..b1b91e90 100644 --- a/frontend/src/components/Header/Header.tsx +++ b/frontend/src/components/Header/Header.tsx @@ -1,65 +1,33 @@ -import React, { useContext, useEffect, useRef, useState } from 'react'; +import React, { useContext, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { AuthContext } from '@contexts/AuthContext'; import ProfileWidget from '@components/ProfileWidget'; +import Menu from './Menu'; +import Search from './Search'; import styles from './Header.module.css'; -import { FiSearch, FiBell, FiSun, FiMoon } from 'react-icons/fi'; const Header: React.FC = () => { const navigate = useNavigate(); const { isLoggedIn, logout, memberProfile } = useContext(AuthContext); const [isProfileWidgetOpen, setProfileWidgetOpen] = useState(false); - const [isSearchInputVisible, setSearchInputVisible] = useState(false); const [isDarkTheme, setIsDarkTheme] = useState(false); const profileButtonRef = useRef(null); const profileWidgetRef = useRef(null); - const searchInputRef = useRef(null); const handleLogout = async (): Promise => { logout(); navigate('/login'); }; - const toggleSearchInput = (event: React.MouseEvent): void => { - event.stopPropagation(); - setSearchInputVisible((prev) => !prev); - }; - const toggleTheme = (): void => { setIsDarkTheme((prev) => !prev); const theme = isDarkTheme ? 'light' : 'dark'; document.documentElement.setAttribute('data-theme', theme); - - const linkElement = document.createElement('link'); - linkElement.rel = 'stylesheet'; - linkElement.href = '/styles/theme.css'; - document.head.appendChild(linkElement); }; - useEffect(() => { - const handleClickOutside = (event: MouseEvent): void => { - const isProfileClick = profileButtonRef.current && profileButtonRef.current.contains(event.target as Node); - const isSearchClick = searchInputRef.current && searchInputRef.current.contains(event.target as Node); - const isProfileWidgetClick = profileWidgetRef.current && profileWidgetRef.current.contains(event.target as Node); - - if (!isProfileClick && !isProfileWidgetClick) { - setProfileWidgetOpen(false); - } - - if (!isSearchClick) { - setSearchInputVisible(false); - } - }; - - window.addEventListener('click', handleClickOutside); - return () => { - window.removeEventListener('click', handleClickOutside); - }; - }, []); - - const handleMouseEnterProfile = (): void => { - setProfileWidgetOpen(true); + const toggleProfileWidget = (): void => { + setProfileWidgetOpen((prev) => !prev); }; return ( @@ -67,58 +35,25 @@ const Header: React.FC = () => {

navigate('/')}>TOONPICK

- +
-
- {isSearchInputVisible && ( - ) => { - if (e.key === 'Enter') { - navigate(`/search?query=${e.currentTarget.value}`); - } - }} - /> - )} - - - - - -
+ -
+
{isLoggedIn ? ( <> void; +} + +const Menu: React.FC = ({ navigate }) => { + return ( + + ); +}; + +export default Menu; \ No newline at end of file diff --git a/frontend/src/components/Header/Search.tsx b/frontend/src/components/Header/Search.tsx new file mode 100644 index 00000000..531965f2 --- /dev/null +++ b/frontend/src/components/Header/Search.tsx @@ -0,0 +1,54 @@ +import React, { useState, useRef } from 'react'; +import { FiSearch, FiSun, FiMoon, FiBell } from 'react-icons/fi'; +import styles from './Header.module.css'; + +interface SearchProps { + navigate: (path: string) => void; + isDarkTheme: boolean; + toggleTheme: () => void; +} + +const Search: React.FC = ({ navigate, isDarkTheme, toggleTheme }) => { + const [isSearchInputVisible, setSearchInputVisible] = useState(false); + const searchInputRef = useRef(null); + + const toggleSearchInput = (event: React.MouseEvent): void => { + event.stopPropagation(); + setSearchInputVisible((prev) => !prev); + }; + + return ( +
+ {isSearchInputVisible && ( + ) => { + if (e.key === 'Enter') { + navigate(`/search?query=${e.currentTarget.value}`); + } + }} + /> + )} + + + + + +
+ ); +}; + +export default Search; \ No newline at end of file diff --git a/frontend/src/components/ProfileWidget/ProfileWidget.tsx b/frontend/src/components/ProfileWidget/ProfileWidget.tsx index 3b2ff20f..5323ddd6 100644 --- a/frontend/src/components/ProfileWidget/ProfileWidget.tsx +++ b/frontend/src/components/ProfileWidget/ProfileWidget.tsx @@ -21,8 +21,13 @@ const ProfileWidget: React.FC = ({ profileButtonRef, profileWidgetRef, }) => { - const handleButtonClick = (action: () => void) => { + const handleButtonClick = (action: () => void, closeWidget: () => void) => (event: React.MouseEvent) => { + event.stopPropagation(); action(); + closeWidget(); + }; + + const closeProfileWidget = () => { setProfileWidgetOpen(false); }; @@ -31,7 +36,7 @@ const ProfileWidget: React.FC = ({ const isProfileWidgetClick = profileWidgetRef.current && profileWidgetRef.current.contains(event.target as Node); if (!isProfileClick && !isProfileWidgetClick) { - setProfileWidgetOpen(false); + closeProfileWidget(); } }, [profileButtonRef, profileWidgetRef, setProfileWidgetOpen]); @@ -60,25 +65,25 @@ const ProfileWidget: React.FC = ({

{memberProfile?.nickname || '게스트 사용자'}

@@ -77,7 +82,6 @@ const SignInPage: React.FC = () => { placeholder="비밀번호" value={formData.password} onChange={handleChange} - required />
@@ -101,9 +105,9 @@ const SignInPage: React.FC = () => {
- handleSocialLogin('google')} /> - handleSocialLogin('kakao')} /> - handleSocialLogin('naver')} /> + handleSocialLogin('google')} type="button" /> + handleSocialLogin('kakao')} type="button" /> + handleSocialLogin('naver')} type="button" />
From f80101a82ec94d0e20b120b7ab1eeb902d41a6d4 Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Sat, 25 Jan 2025 22:36:40 +0900 Subject: [PATCH 46/54] =?UTF-8?q?feat:=20Profile=20Page=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AchievementItem.module.css | 23 +++-- .../AchievementItem/AchievementItem.tsx | 4 + .../LevelDisplay/LevelDisplay.module.css | 16 +-- .../WebtoonList/WebtoonList.module.css | 15 ++- .../components/WebtoonList/WebtoonList.tsx | 47 +++++---- .../MemberProfileSection.module.css | 98 ++++++++++++++++--- .../MemberProfileSection.tsx | 61 ++++++++---- 7 files changed, 191 insertions(+), 73 deletions(-) diff --git a/frontend/src/components/AchievementItem/AchievementItem.module.css b/frontend/src/components/AchievementItem/AchievementItem.module.css index 163c6d88..1f38f746 100644 --- a/frontend/src/components/AchievementItem/AchievementItem.module.css +++ b/frontend/src/components/AchievementItem/AchievementItem.module.css @@ -1,22 +1,24 @@ .achievementItem { display: flex; align-items: center; - background-color: #f9f9f9; + background-color: #ffffff; border: 1px solid #ddd; - border-radius: 8px; - padding: 10px; + border-radius: 12px; + padding: 15px; margin: 10px 0; - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); - transition: transform 0.2s; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; + cursor: pointer; } .achievementItem:hover { - transform: scale(1.02); + transform: scale(1.05); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.2); } .icon { - font-size: 24px; - margin-right: 10px; + font-size: 28px; + margin-right: 15px; } .details { @@ -25,11 +27,12 @@ } .title { - font-size: 16px; + font-size: 18px; + font-weight: bold; color: #333; } .count { - font-size: 14px; + font-size: 16px; color: #666; } \ No newline at end of file diff --git a/frontend/src/components/AchievementItem/AchievementItem.tsx b/frontend/src/components/AchievementItem/AchievementItem.tsx index 584824d2..2e2a8477 100644 --- a/frontend/src/components/AchievementItem/AchievementItem.tsx +++ b/frontend/src/components/AchievementItem/AchievementItem.tsx @@ -1,5 +1,6 @@ import React from 'react'; import styles from './AchievementItem.module.css'; +import { FaTrophy } from 'react-icons/fa'; interface AchievementItemProps { title: string; @@ -9,6 +10,9 @@ interface AchievementItemProps { const AchievementItem: React.FC = ({ title, count }) => { return (
+
+ +
{title} {count} diff --git a/frontend/src/components/LevelDisplay/LevelDisplay.module.css b/frontend/src/components/LevelDisplay/LevelDisplay.module.css index 4ba175d1..f6af7706 100644 --- a/frontend/src/components/LevelDisplay/LevelDisplay.module.css +++ b/frontend/src/components/LevelDisplay/LevelDisplay.module.css @@ -1,27 +1,29 @@ .levelContainer { display: flex; align-items: center; - margin: 10px 0; + margin-top: 10px; + width: 100%; + max-width: 300px; } .progressBar { - width: 100%; - height: 10px; + flex-grow: 1; + height: 8px; background-color: #e0e0e0; - border-radius: 5px; + border-radius: 4px; overflow: hidden; margin-right: 10px; } .progress { height: 100%; - background-color: #4caf50; + background-color: #007bff; transition: width 0.3s ease; } .levelInfo { - display: flex; - align-items: center; + font-size: 14px; + color: #333; } .levelIcon { diff --git a/frontend/src/components/WebtoonList/WebtoonList.module.css b/frontend/src/components/WebtoonList/WebtoonList.module.css index 434d1ba0..ddc02804 100644 --- a/frontend/src/components/WebtoonList/WebtoonList.module.css +++ b/frontend/src/components/WebtoonList/WebtoonList.module.css @@ -2,6 +2,7 @@ padding: 10px; position: relative; overflow: hidden; + min-height: 220px; /* 기본 높이 설정 */ } .carousel { @@ -20,12 +21,17 @@ color: #888; font-size: 18px; margin-top: 20px; + width: 100%; } .navigation { display: flex; - justify-content: center; - margin-top: 10px; + justify-content: space-between; + position: absolute; + top: 50%; + width: 100%; + transform: translateY(-50%); + pointer-events: none; } .navButton { @@ -33,10 +39,9 @@ border: none; font-size: 24px; cursor: pointer; - margin: 0 10px; + pointer-events: auto; } .navButton:disabled { - cursor: not-allowed; - opacity: 0.5; + display: none; } \ No newline at end of file diff --git a/frontend/src/components/WebtoonList/WebtoonList.tsx b/frontend/src/components/WebtoonList/WebtoonList.tsx index dca6cd1f..2dadae92 100644 --- a/frontend/src/components/WebtoonList/WebtoonList.tsx +++ b/frontend/src/components/WebtoonList/WebtoonList.tsx @@ -11,7 +11,7 @@ interface WebtoonListProps { const WebtoonList: React.FC = ({ webtoons, size = 220, showTags = true }) => { const [currentPage, setCurrentPage] = useState(0); - const itemsPerPage = 4; + const itemsPerPage = 5; const totalPages = Math.ceil(webtoons.length / itemsPerPage); const handleNext = () => { @@ -26,30 +26,39 @@ const WebtoonList: React.FC = ({ webtoons, size = 220, showTag } }; - return (
- {webtoons.map((webtoon) => ( - - ))} -
-
- - + {webtoons.length > 0 ? ( + webtoons.map((webtoon) => ( + + )) + ) : ( +
웹툰이 없습니다.
+ )}
+ {webtoons.length > itemsPerPage && ( +
+ {currentPage > 0 && ( + + )} + {currentPage < totalPages - 1 && ( + + )} +
+ )}
); }; diff --git a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.module.css b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.module.css index c9ad2f24..61ac4552 100644 --- a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.module.css +++ b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.module.css @@ -1,12 +1,14 @@ .profile { margin: 20px 0; - padding: 20px; + padding: 30px; border-radius: 12px; background-color: #ffffff; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); transition: box-shadow 0.3s; display: flex; - flex-direction: column; + align-items: flex-start; + height: 270px; + width: 100%; } .profile:hover { @@ -15,23 +17,33 @@ .userInfo { display: flex; - align-items: center; + align-items: flex-start; + width: 100%; } .profilePicture { - width: 80px; - height: 80px; + width: 100px; + height: 100px; border-radius: 50%; - margin-right: 20px; + margin-right: 30px; + margin-left: 10px; + border: 2px solid #ddd; } .profileDetails { display: flex; flex-direction: column; + flex-grow: 1; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; } -.profileDetails h3 { - font-size: 24px; +.header h3 { + font-size: 28px; margin: 0; color: #333; } @@ -44,21 +56,77 @@ .stats { display: flex; - justify-content: space-between; + justify-content: space-around; margin-top: 10px; } .statItem { - background-color: #f0f0f0; - border-radius: 8px; - padding: 10px; - flex: 1; - margin: 0 5px; text-align: center; +} + +.statCount { + font-size: 18px; + font-weight: bold; + color: #333; +} + +.statLabel { + font-size: 14px; + color: #666; +} + +.achievements { + display: flex; + flex-wrap: wrap; + gap: 10px; + margin-top: 15px; + align-items: center; +} + +.iconButton { + background: none; + border: none; + cursor: pointer; + margin-left: 10px; + font-size: 20px; + transition: color 0.3s; +} + +.iconButton:hover { + color: #007bff; +} + +.actions { + display: flex; + justify-content: flex-start; + margin-top: 15px; +} + +.followButton, .editProfileButton { + padding: 8px 16px; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + margin-right: 10px; + border: none; transition: background-color 0.3s; } -.statItem:hover { +.followButton { + background-color: #007bff; + color: white; +} + +.followButton:hover { + background-color: #0056b3; +} + +.editProfileButton { + background-color: #f0f0f0; + color: #333; +} + +.editProfileButton:hover { background-color: #e0e0e0; } diff --git a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx index de73e6b2..2be50a87 100644 --- a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx +++ b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx @@ -1,39 +1,66 @@ -import React from 'react'; +import React, { useState } from 'react'; import styles from './MemberProfileSection.module.css'; -import LevelDisplay from '@components/LevelDisplay'; -import AchievementItem from '@components/AchievementItem'; +import { useNavigate } from 'react-router-dom'; import { MemberProfile } from '@models/member'; +import { FaHeart, FaEdit, FaEllipsisH } from 'react-icons/fa'; +import AchievementItem from '@components/AchievementItem'; +import LevelDisplay from '@components/LevelDisplay'; interface MemberProfileSectionProps { memberProfile: MemberProfile | null; } const MemberProfileSection: React.FC = ({ memberProfile }) => { + const navigate = useNavigate(); + const [isFollowing, setIsFollowing] = useState(false); + const [followerCount, setFollowerCount] = useState(120); // 더미 데이터 + const [followingCount, setFollowingCount] = useState(150); // 더미 데이터 + const [recommendationCount, setRecommendationCount] = useState(30); // 더미 데이터 + const [reviewCount, setReviewCount] = useState(45); // 더미 데이터 + const [averageRating, setAverageRating] = useState(4.5); // 더미 데이터 + + const toggleFollow = () => { + setIsFollowing((prev) => !prev); + setFollowerCount((prev) => (isFollowing ? prev - 1 : prev + 1)); + }; + + const handleEditProfile = () => { + // TODO: 프로필 수정 기능 추가 + }; + + const handleMoreOptions = () => { + // todo: 더보기 기능 추가 + }; + return (
{memberProfile && (
프로필
-

{memberProfile.nickname}

+
+

{memberProfile.nickname}

+
+ + + +
+
-
- - - +
+ + +
From 71ab5d45c103aee7a6ad7f7a988a94f4b8887081 Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Sat, 25 Jan 2025 22:37:00 +0900 Subject: [PATCH 47/54] feat : add profile edit page --- frontend/src/App.tsx | 2 + .../MemberProfileSection.tsx | 1 + .../MemberProfileSection/index.ts | 2 +- .../ProfileEditPage.module.css | 95 +++++++++++++++ .../pages/ProfileEditPage/ProfileEditPage.tsx | 36 ++++++ .../ProfilePictureUpload.module.css | 53 +++++++++ .../ProfileEditPage/ProfilePictureUpload.tsx | 54 +++++++++ .../ProfileEditPage/ProfileSection.module.css | 42 +++++++ .../pages/ProfileEditPage/ProfileSection.tsx | 54 +++++++++ .../SettingsSection.module.css | 42 +++++++ .../pages/ProfileEditPage/SettingsSection.tsx | 111 ++++++++++++++++++ frontend/src/pages/ProfileEditPage/index.ts | 1 + 12 files changed, 492 insertions(+), 1 deletion(-) create mode 100644 frontend/src/pages/ProfileEditPage/ProfileEditPage.module.css create mode 100644 frontend/src/pages/ProfileEditPage/ProfileEditPage.tsx create mode 100644 frontend/src/pages/ProfileEditPage/ProfilePictureUpload.module.css create mode 100644 frontend/src/pages/ProfileEditPage/ProfilePictureUpload.tsx create mode 100644 frontend/src/pages/ProfileEditPage/ProfileSection.module.css create mode 100644 frontend/src/pages/ProfileEditPage/ProfileSection.tsx create mode 100644 frontend/src/pages/ProfileEditPage/SettingsSection.module.css create mode 100644 frontend/src/pages/ProfileEditPage/SettingsSection.tsx create mode 100644 frontend/src/pages/ProfileEditPage/index.ts diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7d1bebf8..2e63e60f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -14,6 +14,7 @@ import SocialLoginCallbackPage from '@pages/auth/SocialLoginCallbackPage'; import NewWebtoonsPage from '@pages/NewWebtoonsPage'; import OngoingWebtoonsPage from '@pages/OngoingWebtoonsPage'; import CompletedWebtoonsPage from '@pages/CompletedWebtoonsPage'; +import ProfileEditPage from '@pages/ProfileEditPage'; import ErrorPage from '@pages/ErrorPage/ErrorPage'; const App: React.FC = () => { @@ -33,6 +34,7 @@ const App: React.FC = () => { } /> } /> } /> + } /> } /> } /> diff --git a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx index 2be50a87..f277896a 100644 --- a/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx +++ b/frontend/src/pages/MyProfilePage/MemberProfileSection/MemberProfileSection.tsx @@ -26,6 +26,7 @@ const MemberProfileSection: React.FC = ({ memberProfi const handleEditProfile = () => { // TODO: 프로필 수정 기능 추가 + navigate('/profile/edit'); }; const handleMoreOptions = () => { diff --git a/frontend/src/pages/MyProfilePage/MemberProfileSection/index.ts b/frontend/src/pages/MyProfilePage/MemberProfileSection/index.ts index 90791067..87ed0f2a 100644 --- a/frontend/src/pages/MyProfilePage/MemberProfileSection/index.ts +++ b/frontend/src/pages/MyProfilePage/MemberProfileSection/index.ts @@ -1 +1 @@ -export { default } from './MemberProfileSection'; +export { default } from './MemberProfileSection'; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfileEditPage.module.css b/frontend/src/pages/ProfileEditPage/ProfileEditPage.module.css new file mode 100644 index 00000000..f0c425d7 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfileEditPage.module.css @@ -0,0 +1,95 @@ +.container { + display: flex; + max-width: 1200px; + margin: 0 auto; + padding: 20px; + background-color: #f9f9f9; + border-radius: 12px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1); +} + +.sidebar { + flex: 1; + padding: 20px; + background-color: #ffffff; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); + margin-right: 20px; +} + +.sidebar h2 { + font-size: 20px; + margin-bottom: 15px; + color: #333; +} + +.sidebar ul { + list-style: none; + padding: 0; +} + +.sidebar li { + padding: 10px; + cursor: pointer; + transition: background-color 0.3s; +} + +.sidebar li:hover { + background-color: #e0e0e0; +} + +.active { + background-color: #007bff; + color: white; +} + +.content { + flex: 3; + padding: 20px; + background-color: #ffffff; + border-radius: 12px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); +} + +h2 { + font-size: 24px; + margin-bottom: 20px; + color: #333; +} + +.profileSection, .settingsSection { + margin-bottom: 40px; +} + +label { + display: block; + margin-bottom: 15px; + font-size: 16px; + color: #666; +} + +.input, .select { + width: 100%; + padding: 10px; + margin-top: 5px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 16px; +} + +.saveButton { + display: block; + width: 100%; + padding: 12px; + background-color: #007bff; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.saveButton:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfileEditPage.tsx b/frontend/src/pages/ProfileEditPage/ProfileEditPage.tsx new file mode 100644 index 00000000..a837cdee --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfileEditPage.tsx @@ -0,0 +1,36 @@ +import React, { useState } from 'react'; +import styles from './ProfileEditPage.module.css'; +import ProfileSection from './ProfileSection'; +import SettingsSection from './SettingsSection'; + +const ProfileEditPage: React.FC = () => { + const [activeSection, setActiveSection] = useState('profile'); + + return ( +
+
+

설정 메뉴

+
    +
  • setActiveSection('profile')} + > + 프로필 수정 +
  • +
  • setActiveSection('settings')} + > + 개인 설정 +
  • +
+
+
+ {activeSection === 'profile' && } + {activeSection === 'settings' && } +
+
+ ); +}; + +export default ProfileEditPage; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.module.css b/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.module.css new file mode 100644 index 00000000..e62d831e --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.module.css @@ -0,0 +1,53 @@ +.uploadContainer { + margin-bottom: 40px; +} + +.preview { + width: 100%; + height: 200px; + border: 1px dashed #ddd; + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 15px; + background-color: #f9f9f9; +} + +.previewImage { + max-width: 100%; + max-height: 100%; + border-radius: 8px; +} + +.placeholder { + color: #aaa; +} + +.fileInputLabel { + display: block; + margin-bottom: 15px; + font-size: 16px; + color: #666; +} + +.fileInput { + display: none; /* 숨김 처리 */ +} + +.saveButton { + display: block; + width: 100%; + padding: 12px; + background-color: #007bff; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.saveButton:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.tsx b/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.tsx new file mode 100644 index 00000000..f2e5fbcd --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfilePictureUpload.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import styles from './ProfilePictureUpload.module.css'; + +const ProfilePictureUpload: React.FC<{ onSave: (file: File | null) => void }> = ({ onSave }) => { + const [selectedFile, setSelectedFile] = useState(null); + const [previewUrl, setPreviewUrl] = useState(null); + + const handleFileChange = (event: React.ChangeEvent) => { + const file = event.target.files ? event.target.files[0] : null; + setSelectedFile(file); + if (file) { + const reader = new FileReader(); + reader.onloadend = () => { + setPreviewUrl(reader.result as string); + }; + reader.readAsDataURL(file); + } else { + setPreviewUrl(null); + } + }; + + const handleSave = () => { + if (selectedFile) { + onSave(selectedFile); + } + }; + + return ( +
+

프로필 사진 등록/수정

+
+ {previewUrl ? ( + 미리보기 + ) : ( +
미리보기 없음
+ )} +
+ + +
+ ); +}; + +export default ProfilePictureUpload; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfileSection.module.css b/frontend/src/pages/ProfileEditPage/ProfileSection.module.css new file mode 100644 index 00000000..7133f117 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfileSection.module.css @@ -0,0 +1,42 @@ +.profileSection { + margin-bottom: 40px; +} + +h2 { + font-size: 24px; + margin-bottom: 20px; + color: #333; +} + +label { + display: block; + margin-bottom: 15px; + font-size: 16px; + color: #666; +} + +.input { + width: 100%; + padding: 10px; + margin-top: 5px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 16px; +} + +.saveButton { + display: block; + width: 100%; + padding: 12px; + background-color: #007bff; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.saveButton:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/ProfileSection.tsx b/frontend/src/pages/ProfileEditPage/ProfileSection.tsx new file mode 100644 index 00000000..701de607 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/ProfileSection.tsx @@ -0,0 +1,54 @@ +import React, { useState } from 'react'; +import styles from './ProfileSection.module.css'; +import ProfilePictureUpload from './ProfilePictureUpload'; + +const ProfileSection: React.FC = () => { + const [nickname, setNickname] = useState('사용자 닉네임'); + const [email, setEmail] = useState('user@example.com'); + const [password, setPassword] = useState(''); + const [profilePicture, setProfilePicture] = useState(null); + + const handleSaveProfile = () => { + // TODO: 프로필 저장 로직 추가 + console.log('프로필 저장:', { nickname, email, password, profilePicture }); + }; + + return ( +
+

프로필 수정

+ + + + + +
+ ); +}; + +export default ProfileSection; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/SettingsSection.module.css b/frontend/src/pages/ProfileEditPage/SettingsSection.module.css new file mode 100644 index 00000000..b6ddb712 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/SettingsSection.module.css @@ -0,0 +1,42 @@ +.settingsSection { + margin-bottom: 40px; +} + +h2 { + font-size: 24px; + margin-bottom: 20px; + color: #333; +} + +label { + display: block; + margin-bottom: 15px; + font-size: 16px; + color: #666; +} + +.select { + width: 100%; + padding: 10px; + margin-top: 5px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 16px; +} + +.saveButton { + display: block; + width: 100%; + padding: 12px; + background-color: #007bff; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + transition: background-color 0.3s; +} + +.saveButton:hover { + background-color: #0056b3; +} \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/SettingsSection.tsx b/frontend/src/pages/ProfileEditPage/SettingsSection.tsx new file mode 100644 index 00000000..a0b143d6 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/SettingsSection.tsx @@ -0,0 +1,111 @@ +import React, { useState } from 'react'; +import styles from './SettingsSection.module.css'; + +const SettingsSection: React.FC = () => { + const [notificationsEnabled, setNotificationsEnabled] = useState(true); + const [profileVisibility, setProfileVisibility] = useState('public'); + const [adultVerification, setAdultVerification] = useState(false); + const [commentVisibility, setCommentVisibility] = useState('public'); + const [ratingVisibility, setRatingVisibility] = useState('public'); + const [webtoonPreferenceVisibility, setWebtoonPreferenceVisibility] = useState('public'); + const [recommendedWebtoonRange, setRecommendedWebtoonRange] = useState('all'); + const [hiddenUsers, setHiddenUsers] = useState([]); + const [blockedUsers, setBlockedUsers] = useState([]); + + const handleSaveSettings = () => { + // TODO: 설정 저장 로직 추가 + console.log('설정 저장:', { + notificationsEnabled, + profileVisibility, + adultVerification, + commentVisibility, + ratingVisibility, + webtoonPreferenceVisibility, + recommendedWebtoonRange, + hiddenUsers, + blockedUsers, + }); + }; + + return ( +
+

개인 설정

+ + + + + + + + +
+ ); +}; + +export default SettingsSection; \ No newline at end of file diff --git a/frontend/src/pages/ProfileEditPage/index.ts b/frontend/src/pages/ProfileEditPage/index.ts new file mode 100644 index 00000000..f8054ca3 --- /dev/null +++ b/frontend/src/pages/ProfileEditPage/index.ts @@ -0,0 +1 @@ +export { default } from './ProfileEditPage'; \ No newline at end of file From 62455a7e425980129228d74e88f467f338fc5f45 Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Sat, 25 Jan 2025 22:37:00 +0900 Subject: [PATCH 48/54] feat : add profile edit page --- frontend/src/pages/ProfileEditPage/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/pages/ProfileEditPage/index.ts b/frontend/src/pages/ProfileEditPage/index.ts index f8054ca3..24d9d731 100644 --- a/frontend/src/pages/ProfileEditPage/index.ts +++ b/frontend/src/pages/ProfileEditPage/index.ts @@ -1 +1 @@ -export { default } from './ProfileEditPage'; \ No newline at end of file +export { default } from './ProfileEditPage'; From 5e9de2ec959c6290172041b7122a4ede9c161cbb Mon Sep 17 00:00:00 2001 From: ImGdevel Date: Sun, 2 Feb 2025 17:33:35 +0900 Subject: [PATCH 49/54] feat: docker config --- backend/docker-compose.yml | 76 +++++++++++++++++++-- backend/logs/info_2024-12-27.gz | Bin 16373 -> 0 bytes backend/logs/info_2025-01-03.gz | Bin 14033 -> 0 bytes backend/logs/info_2025-01-28.gz | Bin 0 -> 12473 bytes backend/src/main/resources/application.yml | 6 +- 5 files changed, 74 insertions(+), 8 deletions(-) delete mode 100644 backend/logs/info_2024-12-27.gz delete mode 100644 backend/logs/info_2025-01-03.gz create mode 100644 backend/logs/info_2025-01-28.gz diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 126b1bc7..0fabdbbf 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -1,19 +1,85 @@ -version: "3.9" - services: spring-app: image: toonpick-service-app:0.0.1 container_name: toonpick-service-app - build: + build: context: . dockerfile: Dockerfile ports: - "8080:8080" volumes: - ./logs:/app/logs - environment: LOG_FILE: /app/logs/application.log - + SPRING_DATASOURCE_META_URL: jdbc:mariadb://toonpick-db-meta:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + SPRING_DATASOURCE_META_USERNAME: root + SPRING_DATASOURCE_META_PASSWORD: 1234 + SPRING_DATASOURCE_DATA_URL: jdbc:mariadb://toonpick-db:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + SPRING_DATASOURCE_DATA_USERNAME: root + SPRING_DATASOURCE_DATA_PASSWORD: 1234 restart: always + depends_on: + mariadb: + condition: service_healthy + mariadb-meta: + condition: service_healthy + redis: + condition: service_started + networks: + - backend + + mariadb: + image: mariadb:10.5 + container_name: toonpick-db + environment: + MYSQL_ROOT_PASSWORD: 1234 + MYSQL_DATABASE: toonpick-database + ports: + - "3306:3306" + volumes: + - mariadb-data:/var/lib/mysql + networks: + - backend + healthcheck: + test: ["CMD", "mysqladmin", "ping", "--host=localhost", "--user=root", "--password=1234"] + interval: 30s + retries: 5 + timeout: 10s + start_period: 30s + + mariadb-meta: + image: mariadb:10.5 + container_name: toonpick-db-meta + environment: + MYSQL_ROOT_PASSWORD: 1234 + MYSQL_DATABASE: meta + ports: + - "3307:3306" + volumes: + - mariadb-meta-data:/var/lib/mysql + networks: + - backend + healthcheck: + test: ["CMD", "mysqladmin", "ping", "--host=localhost", "--user=root", "--password=1234"] + interval: 30s + retries: 5 + timeout: 10s + start_period: 30s + + redis: + image: redis:6.0 + container_name: toonpick-redis + ports: + - "6380:6379" + networks: + - backend + +volumes: + mariadb-data: + driver: local + mariadb-meta-data: + driver: local +networks: + backend: + driver: bridge diff --git a/backend/logs/info_2024-12-27.gz b/backend/logs/info_2024-12-27.gz deleted file mode 100644 index d89fc6c9cfead057198da87b4dc5d58be39bc337..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16373 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 diff --git a/backend/logs/info_2025-01-28.gz b/backend/logs/info_2025-01-28.gz new file mode 100644 index 0000000000000000000000000000000000000000..7e181617e2fdfd1693b47e8b75a84f32d71226e4 GIT binary patch literal 12473 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 literal 0 HcmV?d00001 diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index af384864..a943a035 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -7,18 +7,18 @@ spring: datasource-meta: driver-class-name: org.mariadb.jdbc.Driver - jdbc-url: jdbc:mariadb://localhost:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + jdbc-url: jdbc:mariadb://mariadb:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true username: root password: 1234 datasource-data: driver-class-name: org.mariadb.jdbc.Driver - jdbc-url: jdbc:mariadb://localhost:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + jdbc-url: jdbc:mariadb://mariadb:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true username: root password: 1234 redis: - host: localhost + host: redis port: 6380 timeout: 10000ms jedis: From 62f43de5de50a8a8577c41baddbaaa0ad7e2c8ad Mon Sep 17 00:00:00 2001 From: ImGdevel Date: Sun, 2 Feb 2025 17:42:21 +0900 Subject: [PATCH 50/54] fix: fix db url --- backend/src/main/resources/application.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index a943a035..693842b3 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -7,13 +7,13 @@ spring: datasource-meta: driver-class-name: org.mariadb.jdbc.Driver - jdbc-url: jdbc:mariadb://mariadb:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + jdbc-url: jdbc:mariadb://toonpick-db-meta:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true username: root password: 1234 datasource-data: driver-class-name: org.mariadb.jdbc.Driver - jdbc-url: jdbc:mariadb://mariadb:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + jdbc-url: jdbc:mariadb://toonpick-db:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true username: root password: 1234 From f4ef5fc95cbcddc37a9746ddeca5e1224d45afd4 Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Sun, 2 Feb 2025 17:48:06 +0900 Subject: [PATCH 51/54] feat: add ping test controller --- .../app/controller/HelloController.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 backend/src/main/java/toonpick/app/controller/HelloController.java diff --git a/backend/src/main/java/toonpick/app/controller/HelloController.java b/backend/src/main/java/toonpick/app/controller/HelloController.java new file mode 100644 index 00000000..fb9e92b8 --- /dev/null +++ b/backend/src/main/java/toonpick/app/controller/HelloController.java @@ -0,0 +1,31 @@ +package toonpick.app.controller; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +@Controller +public class HelloController { + + final private Logger logger = LoggerFactory.getLogger(HelloController.class); + + @GetMapping("/api/public/ping") + private ResponseEntity> testPing() { + Map response = new HashMap<>(); + + response.put("status", "success"); + response.put("message", "TOONPICK 서비스가 정상적으로 작동 중입니다."); + response.put("timestamp", LocalDateTime.now()); + + logger.info("Ping 요청이 들어왔습니다. 응답: {}", response); + + return ResponseEntity.status(HttpStatus.OK).body(response); + } +} From fe431f076054b7de2dbbf10da3008856760e556f Mon Sep 17 00:00:00 2001 From: ImGdevel Date: Sun, 2 Feb 2025 18:36:08 +0900 Subject: [PATCH 52/54] feat: db congif env --- backend/docker-compose.yml | 6 ++--- .../toonpick/app/config/DataDBConfig.java | 2 +- .../toonpick/app/config/MetaDBConfig.java | 2 +- backend/src/main/resources/application.yml | 22 +++++++++---------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml index 0fabdbbf..98490227 100644 --- a/backend/docker-compose.yml +++ b/backend/docker-compose.yml @@ -11,12 +11,12 @@ services: - ./logs:/app/logs environment: LOG_FILE: /app/logs/application.log - SPRING_DATASOURCE_META_URL: jdbc:mariadb://toonpick-db-meta:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true - SPRING_DATASOURCE_META_USERNAME: root - SPRING_DATASOURCE_META_PASSWORD: 1234 SPRING_DATASOURCE_DATA_URL: jdbc:mariadb://toonpick-db:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true SPRING_DATASOURCE_DATA_USERNAME: root SPRING_DATASOURCE_DATA_PASSWORD: 1234 + SPRING_DATASOURCE_META_URL: jdbc:mariadb://toonpick-db-meta:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true + SPRING_DATASOURCE_META_USERNAME: root + SPRING_DATASOURCE_META_PASSWORD: 1234 restart: always depends_on: mariadb: diff --git a/backend/src/main/java/toonpick/app/config/DataDBConfig.java b/backend/src/main/java/toonpick/app/config/DataDBConfig.java index 79c66dde..0f122fa7 100644 --- a/backend/src/main/java/toonpick/app/config/DataDBConfig.java +++ b/backend/src/main/java/toonpick/app/config/DataDBConfig.java @@ -23,7 +23,7 @@ public class DataDBConfig { @Bean - @ConfigurationProperties(prefix = "spring.datasource-data") + @ConfigurationProperties(prefix = "spring.datasource.data") public DataSource dataDBSource() { return DataSourceBuilder.create().build(); } diff --git a/backend/src/main/java/toonpick/app/config/MetaDBConfig.java b/backend/src/main/java/toonpick/app/config/MetaDBConfig.java index 3caeac82..958a250f 100644 --- a/backend/src/main/java/toonpick/app/config/MetaDBConfig.java +++ b/backend/src/main/java/toonpick/app/config/MetaDBConfig.java @@ -16,7 +16,7 @@ public class MetaDBConfig { @Primary @Bean - @ConfigurationProperties(prefix = "spring.datasource-meta") + @ConfigurationProperties(prefix = "spring.datasource.meta") public DataSource metaDBSource(){ return DataSourceBuilder.create().build(); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index 693842b3..99843780 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -5,17 +5,17 @@ spring: config: import: application-API-KEY.yml - datasource-meta: - driver-class-name: org.mariadb.jdbc.Driver - jdbc-url: jdbc:mariadb://toonpick-db-meta:3306/meta?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true - username: root - password: 1234 - - datasource-data: - driver-class-name: org.mariadb.jdbc.Driver - jdbc-url: jdbc:mariadb://toonpick-db:3306/toonpick-database?useSSL=false&useUnicode=true&serverTimezone=Asia/Seoul&allowPublicKeyRetrieval=true - username: root - password: 1234 + datasource: + data: + driver-class-name: org.mariadb.jdbc.Driver + jdbc-url: ${SPRING_DATASOURCE_DATA_URL} + username: ${SPRING_DATASOURCE_DATA_USERNAME} + password: ${SPRING_DATASOURCE_DATA_PASSWORD} + meta: + driver-class-name: org.mariadb.jdbc.Driver + jdbc-url: ${SPRING_DATASOURCE_META_URL} + username: ${SPRING_DATASOURCE_META_USERNAME} + password: ${SPRING_DATASOURCE_META_PASSWORD} redis: host: redis From a9b9fced2ae48c01def406c4417da826f0a7fab0 Mon Sep 17 00:00:00 2001 From: ImGdevel Date: Mon, 3 Feb 2025 13:47:02 +0900 Subject: [PATCH 53/54] feat: github action ci-cd --- .github/workflows/backend-ci-cd.yml | 77 +++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 .github/workflows/backend-ci-cd.yml diff --git a/.github/workflows/backend-ci-cd.yml b/.github/workflows/backend-ci-cd.yml new file mode 100644 index 00000000..a531e5ef --- /dev/null +++ b/.github/workflows/backend-ci-cd.yml @@ -0,0 +1,77 @@ +name: Backend CI/CD + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + + steps: + - name: 저장소 체크아웃 + uses: actions/checkout@v3 + + - name: JDK 17 설정 + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + + - name: Gradle 실행 권한 부여 + run: chmod +x gradlew + + - name: 프로젝트 빌드 + run: ./gradlew build -x test + + - name: 테스트 실행 + run: ./gradlew test + + - name: 빌드 결과 업로드 (Artifact) + uses: actions/upload-artifact@v3 + with: + name: backend-build + path: backend/build/libs/*.jar + + docker: + needs: build + runs-on: ubuntu-latest + defaults: + run: + working-directory: backend + + steps: + - name: 저장소 체크아웃 + uses: actions/checkout@v3 + + - name: Docker 로그인 + run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin + + - name: Docker 이미지 빌드 및 푸시 + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/backend:latest . + docker push ${{ secrets.DOCKER_USERNAME }}/backend:latest + + deploy: + needs: docker + runs-on: ubuntu-latest + + steps: + - name: SSH로 서버 접속 및 배포 + uses: appleboy/ssh-action@v0.1.6 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + script: | + docker pull ${{ secrets.DOCKER_USERNAME }}/toonpick-service-app:latest + docker stop toonpick-service-app || true + docker rm toonpick-service-app || true + docker run -d -p 8080:8080 --env-file /home/${{ secrets.SSH_USER }}/.backend_env --name toonpick-service-app ${{ secrets.DOCKER_USERNAME }}/toonpick-service-app:latest From e7521e9f73fc9d196b52c470dcef89ff2f7e538e Mon Sep 17 00:00:00 2001 From: SH Woo <62339794+ImGdevel@users.noreply.github.com> Date: Mon, 3 Feb 2025 14:33:07 +0900 Subject: [PATCH 54/54] =?UTF-8?q?feat=20:=20=EC=9A=B0=EC=84=A0=20=EB=B9=8C?= =?UTF-8?q?=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/backend-build.yml | 34 +++++++++++++ .github/workflows/backend-ci-cd.yml | 77 ----------------------------- 2 files changed, 34 insertions(+), 77 deletions(-) create mode 100644 .github/workflows/backend-build.yml delete mode 100644 .github/workflows/backend-ci-cd.yml diff --git a/.github/workflows/backend-build.yml b/.github/workflows/backend-build.yml new file mode 100644 index 00000000..274b0516 --- /dev/null +++ b/.github/workflows/backend-build.yml @@ -0,0 +1,34 @@ +name: Backend Build + +on: + push: + branches: + - main + - develop + pull_request: + branches: + - main + - develop + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository (저장소 체크아웃) + uses: actions/checkout@v3 + + - name: Set up JDK 17 (JDK 17 설정) + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' + cache: 'gradle' + + - name: Grant execute permission for gradlew (Gradle 실행 권한 부여) + run: chmod +x backend/gradlew + + - name: 프로젝트 빌드 + run: | + cd backend + ./gradlew clean build -x test diff --git a/.github/workflows/backend-ci-cd.yml b/.github/workflows/backend-ci-cd.yml deleted file mode 100644 index a531e5ef..00000000 --- a/.github/workflows/backend-ci-cd.yml +++ /dev/null @@ -1,77 +0,0 @@ -name: Backend CI/CD - -on: - push: - branches: - - main - pull_request: - branches: - - main - -jobs: - build: - runs-on: ubuntu-latest - defaults: - run: - working-directory: backend - - steps: - - name: 저장소 체크아웃 - uses: actions/checkout@v3 - - - name: JDK 17 설정 - uses: actions/setup-java@v3 - with: - distribution: 'temurin' - java-version: '17' - - - name: Gradle 실행 권한 부여 - run: chmod +x gradlew - - - name: 프로젝트 빌드 - run: ./gradlew build -x test - - - name: 테스트 실행 - run: ./gradlew test - - - name: 빌드 결과 업로드 (Artifact) - uses: actions/upload-artifact@v3 - with: - name: backend-build - path: backend/build/libs/*.jar - - docker: - needs: build - runs-on: ubuntu-latest - defaults: - run: - working-directory: backend - - steps: - - name: 저장소 체크아웃 - uses: actions/checkout@v3 - - - name: Docker 로그인 - run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin - - - name: Docker 이미지 빌드 및 푸시 - run: | - docker build -t ${{ secrets.DOCKER_USERNAME }}/backend:latest . - docker push ${{ secrets.DOCKER_USERNAME }}/backend:latest - - deploy: - needs: docker - runs-on: ubuntu-latest - - steps: - - name: SSH로 서버 접속 및 배포 - uses: appleboy/ssh-action@v0.1.6 - with: - host: ${{ secrets.SSH_HOST }} - username: ${{ secrets.SSH_USER }} - key: ${{ secrets.SSH_PRIVATE_KEY }} - script: | - docker pull ${{ secrets.DOCKER_USERNAME }}/toonpick-service-app:latest - docker stop toonpick-service-app || true - docker rm toonpick-service-app || true - docker run -d -p 8080:8080 --env-file /home/${{ secrets.SSH_USER }}/.backend_env --name toonpick-service-app ${{ secrets.DOCKER_USERNAME }}/toonpick-service-app:latest