├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── java │ │ └── org │ │ │ └── terning │ │ │ └── terningserver │ │ │ ├── user │ │ │ ├── domain │ │ │ │ ├── AuthType.java │ │ │ │ ├── State.java │ │ │ │ ├── Token.java │ │ │ │ ├── ProfileImage.java │ │ │ │ ├── PushNotificationStatus.java │ │ │ │ └── User.java │ │ │ ├── dto │ │ │ │ ├── request │ │ │ │ │ ├── PushStatusUpdateRequest.java │ │ │ │ │ └── ProfileUpdateRequestDto.java │ │ │ │ └── response │ │ │ │ │ └── ProfileResponseDto.java │ │ │ ├── event │ │ │ │ └── UserSignedUpEvent.java │ │ │ ├── application │ │ │ │ ├── UserService.java │ │ │ │ └── UserServiceImpl.java │ │ │ ├── repository │ │ │ │ └── UserRepository.java │ │ │ └── api │ │ │ │ ├── UserSwagger.java │ │ │ │ └── UserProfileController.java │ │ │ ├── auth │ │ │ ├── dto │ │ │ │ ├── Token.java │ │ │ │ ├── response │ │ │ │ │ ├── TokenReissueResponse.java │ │ │ │ │ ├── SignUpResponse.java │ │ │ │ │ └── SignInResponse.java │ │ │ │ └── request │ │ │ │ │ ├── FcmTokenSyncRequest.java │ │ │ │ │ ├── SignUpRequest.java │ │ │ │ │ ├── SignUpFilterRequest.java │ │ │ │ │ └── SignInRequest.java │ │ │ ├── application │ │ │ │ └── social │ │ │ │ │ ├── SocialAuthProvider.java │ │ │ │ │ ├── apple │ │ │ │ │ └── AppleAuthProvider.java │ │ │ │ │ ├── kakao │ │ │ │ │ ├── KakaoAuthProvider.java │ │ │ │ │ └── KakaoAuthTokenValidator.java │ │ │ │ │ └── SocialAuthServiceManager.java │ │ │ ├── config │ │ │ │ ├── Login.java │ │ │ │ ├── LoginUserArgumentResolver.java │ │ │ │ └── LoginCheckInterceptor.java │ │ │ ├── jwt │ │ │ │ └── exception │ │ │ │ │ ├── JwtException.java │ │ │ │ │ └── JwtErrorCode.java │ │ │ ├── common │ │ │ │ ├── exception │ │ │ │ │ ├── AuthException.java │ │ │ │ │ └── AuthErrorCode.java │ │ │ │ └── success │ │ │ │ │ └── AuthSuccessCode.java │ │ │ └── api │ │ │ │ ├── AuthSwagger.java │ │ │ │ └── AuthController.java │ │ │ ├── scrap │ │ │ ├── dto │ │ │ │ └── request │ │ │ │ │ ├── UpdateScrapRequestDto.java │ │ │ │ │ └── CreateScrapRequestDto.java │ │ │ ├── repository │ │ │ │ ├── ScrapRepository.java │ │ │ │ ├── ScrapRepositoryCustom.java │ │ │ │ └── ScrapRepositoryImpl.java │ │ │ ├── domain │ │ │ │ ├── SyncStatus.java │ │ │ │ ├── Color.java │ │ │ │ └── Scrap.java │ │ │ ├── application │ │ │ │ └── ScrapService.java │ │ │ └── api │ │ │ │ ├── ScrapSwagger.java │ │ │ │ └── ScrapController.java │ │ │ ├── common │ │ │ ├── exception │ │ │ │ ├── SuccessCode.java │ │ │ │ ├── CustomException.java │ │ │ │ ├── dto │ │ │ │ │ ├── SuccessResponse.java │ │ │ │ │ └── ErrorResponse.java │ │ │ │ └── enums │ │ │ │ │ ├── ErrorMessage.java │ │ │ │ │ └── SuccessMessage.java │ │ │ ├── util │ │ │ │ ├── LogExecutionTime.java │ │ │ │ ├── DateUtil.java │ │ │ │ └── LogAspect.java │ │ │ ├── config │ │ │ │ ├── RestTemplateConfig.java │ │ │ │ ├── QuerydslConfig.java │ │ │ │ ├── AuthServiceConfig.java │ │ │ │ ├── ValueConfig.java │ │ │ │ ├── SwaggerConfig.java │ │ │ │ └── WebConfig.java │ │ │ ├── BaseTimeEntity.java │ │ │ └── logging │ │ │ │ ├── CachedHttpServletRequest.java │ │ │ │ └── LoggingFilter.java │ │ │ ├── external │ │ │ ├── pushNotification │ │ │ │ ├── scrap │ │ │ │ │ ├── application │ │ │ │ │ │ ├── usecase │ │ │ │ │ │ │ ├── SyncUnsyncedUsersUseCase.java │ │ │ │ │ │ │ ├── ReadUnsyncedScrapUsersUseCase.java │ │ │ │ │ │ │ └── ScrapSyncOrchestrator.java │ │ │ │ │ │ ├── port │ │ │ │ │ │ │ ├── ScrapSyncMarker.java │ │ │ │ │ │ │ ├── ScrapSyncNotifier.java │ │ │ │ │ │ │ ├── UnsyncedScrapMarker.java │ │ │ │ │ │ │ └── UnsyncedScrapUserReader.java │ │ │ │ │ │ └── service │ │ │ │ │ │ │ ├── UnsyncedScrapUserReaderService.java │ │ │ │ │ │ │ ├── ReadUnsyncedScrapUsersService.java │ │ │ │ │ │ │ ├── UnsyncedScrapMarkerService.java │ │ │ │ │ │ │ ├── SyncUnsyncedUsersService.java │ │ │ │ │ │ │ └── ScrapSyncCoordinatorService.java │ │ │ │ │ ├── dto │ │ │ │ │ │ ├── response │ │ │ │ │ │ │ ├── UnsyncedScrapUserResponse.java │ │ │ │ │ │ │ └── UnsyncedScrapUsersResponse.java │ │ │ │ │ │ └── request │ │ │ │ │ │ │ └── ScrapUserIdsRequest.java │ │ │ │ │ ├── common │ │ │ │ │ │ └── failure │ │ │ │ │ │ │ ├── ScrapExternalException.java │ │ │ │ │ │ │ └── ScrapExternalErrorCode.java │ │ │ │ │ ├── scheduler │ │ │ │ │ │ └── ScrapSyncScheduler.java │ │ │ │ │ ├── ScrapSyncMarkerImpl.java │ │ │ │ │ ├── api │ │ │ │ │ │ └── ScrapExternalApiController.java │ │ │ │ │ └── config │ │ │ │ │ │ └── OpsApiClient.java │ │ │ │ ├── user │ │ │ │ │ ├── domain │ │ │ │ │ │ ├── UserSyncEventType.java │ │ │ │ │ │ └── UserSyncEvent.java │ │ │ │ │ ├── application │ │ │ │ │ │ ├── UserSyncEventService.java │ │ │ │ │ │ └── UserSyncEvnetServiceImpl.java │ │ │ │ │ ├── respository │ │ │ │ │ │ └── UserSyncEventRepository.java │ │ │ │ │ ├── config │ │ │ │ │ │ ├── UserSyncJobConfig.java │ │ │ │ │ │ └── NotificationSyncTasklet.java │ │ │ │ │ └── scheduler │ │ │ │ │ │ └── UserSyncScheduler.java │ │ │ │ ├── dto │ │ │ │ │ ├── FcmTokenReissueRequiredResponse.java │ │ │ │ │ └── request │ │ │ │ │ │ └── CreateUserRequest.java │ │ │ │ ├── notification │ │ │ │ │ ├── FcmTokenValidationClient.java │ │ │ │ │ └── NotificationUserClient.java │ │ │ │ └── config │ │ │ │ │ └── WebClientConfig.java │ │ │ └── discord │ │ │ │ └── application │ │ │ │ └── WebhookService.java │ │ │ ├── banner │ │ │ ├── application │ │ │ │ ├── BannerService.java │ │ │ │ └── BannerServiceImpl.java │ │ │ ├── repository │ │ │ │ └── BannerRepository.java │ │ │ ├── api │ │ │ │ ├── BannerSwagger.java │ │ │ │ └── BannerController.java │ │ │ ├── domain │ │ │ │ └── Banner.java │ │ │ └── dto │ │ │ │ └── response │ │ │ │ └── BannerListResponseDto.java │ │ │ ├── filter │ │ │ ├── dto │ │ │ │ ├── request │ │ │ │ │ └── UpdateUserFilterRequestDto.java │ │ │ │ └── response │ │ │ │ │ └── UserFilterDetailResponseDto.java │ │ │ ├── repository │ │ │ │ └── FilterRepository.java │ │ │ ├── application │ │ │ │ ├── FilterService.java │ │ │ │ └── FilterServiceImpl.java │ │ │ ├── domain │ │ │ │ ├── Grade.java │ │ │ │ ├── JobType.java │ │ │ │ ├── WorkingPeriod.java │ │ │ │ └── Filter.java │ │ │ └── api │ │ │ │ ├── FilterSwagger.java │ │ │ │ └── FilterController.java │ │ │ ├── home │ │ │ ├── application │ │ │ │ └── HomeService.java │ │ │ ├── dto │ │ │ │ └── response │ │ │ │ │ ├── HomeAnnouncementsResponseDto.java │ │ │ │ │ ├── HomeResponseDto.java │ │ │ │ │ └── UpcomingScrapResponseDto.java │ │ │ └── api │ │ │ │ ├── HomeSwagger.java │ │ │ │ └── HomeController.java │ │ │ ├── internshipAnnouncement │ │ │ ├── application │ │ │ │ ├── InternshipDetailService.java │ │ │ │ └── InternshipDetailServiceImpl.java │ │ │ ├── repository │ │ │ │ ├── InternshipRepository.java │ │ │ │ └── InternshipRepositoryCustom.java │ │ │ ├── domain │ │ │ │ ├── Company.java │ │ │ │ ├── CompanyCategory.java │ │ │ │ └── InternshipAnnouncement.java │ │ │ ├── api │ │ │ │ ├── InternshipDetailSwagger.java │ │ │ │ └── InternshipDetailController.java │ │ │ └── dto │ │ │ │ └── response │ │ │ │ └── InternshipDetailResponseDto.java │ │ │ ├── TerningserverApplication.java │ │ │ ├── search │ │ │ ├── application │ │ │ │ ├── SearchService.java │ │ │ │ └── SearchServiceImpl.java │ │ │ ├── dto │ │ │ │ └── response │ │ │ │ │ ├── PopularAnnouncementListResponseDto.java │ │ │ │ │ └── SearchResultResponseDto.java │ │ │ └── api │ │ │ │ ├── SearchSwagger.java │ │ │ │ └── SearchController.java │ │ │ └── calendar │ │ │ ├── dto │ │ │ └── response │ │ │ │ ├── MonthlyDefaultResponseDto.java │ │ │ │ ├── DailyScrapResponseDto.java │ │ │ │ └── MonthlyListResponseDto.java │ │ │ └── api │ │ │ ├── CalendarSwagger.java │ │ │ └── CalendarController.java │ └── resources │ │ ├── logback-local.xml │ │ ├── logback-dev.xml │ │ └── logback-prod.xml └── test │ └── java │ └── org │ └── terning │ └── terningserver │ └── TerningserverApplicationTests.java ├── Dockerfile ├── Dockerfile-staging ├── .github ├── pull_request_template.md ├── ISSUE_TEMPLATE │ ├── 💡-issue-template.md │ └── 💡-issue-template-Feat.yml └── workflows │ ├── DEV-CI.yml │ ├── DOCKER-CD.yml │ └── DOCKER-CD-STAGING.yml ├── gradlew.bat └── .gitignore /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'terningserver' 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/teamterning/Terning-Server/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/domain/AuthType.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.domain; 2 | 3 | public enum AuthType { 4 | KAKAO, APPLE 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/dto/Token.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.dto; 2 | 3 | public record Token(String accessToken, String refreshToken) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/dto/response/TokenReissueResponse.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.dto.response; 2 | 3 | public record TokenReissueResponse(String accessToken) { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/dto/request/FcmTokenSyncRequest.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.dto.request; 2 | 3 | public record FcmTokenSyncRequest( 4 | String fcmToken 5 | ) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/scrap/dto/request/UpdateScrapRequestDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.scrap.dto.request; 2 | 3 | public record UpdateScrapRequestDto( 4 | String color 5 | ) { 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/scrap/dto/request/CreateScrapRequestDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.scrap.dto.request; 2 | 3 | public record CreateScrapRequestDto( 4 | String color 5 | ) { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/domain/State.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.domain; 2 | 3 | public enum State { 4 | //활성화(active), 비활성화(inactive), 정지(banned) 5 | ACTIVE, INACTIVE, BANNED 6 | } 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:21-jdk 2 | WORKDIR /app 3 | COPY build/libs/terningserver-0.0.1-SNAPSHOT.jar /app/terningserver.jar 4 | CMD ["java", "-Duser.timezone=Asia/Seoul", "-jar", "-Dspring.profiles.active=prod", "terningserver.jar"] 5 | -------------------------------------------------------------------------------- /Dockerfile-staging: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:21-jdk 2 | WORKDIR /app 3 | COPY build/libs/terningserver-0.0.1-SNAPSHOT.jar /app/terningserver.jar 4 | CMD ["java", "-Duser.timezone=Asia/Seoul", "-jar", "-Dspring.profiles.active=staging", "terningserver.jar"] 5 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/exception/SuccessCode.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.exception; 2 | 3 | import org.springframework.http.HttpStatus; 4 | 5 | public interface SuccessCode { 6 | HttpStatus getStatus(); 7 | String getMessage(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/application/usecase/SyncUnsyncedUsersUseCase.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.application.usecase; 2 | 3 | public interface SyncUnsyncedUsersUseCase { 4 | void sync(); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/user/domain/UserSyncEventType.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.user.domain; 2 | 3 | public enum UserSyncEventType { 4 | PUSH_STATUS_CHANGE, 5 | NAME_CHANGE, 6 | WITHDRAW 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/dto/request/SignUpRequest.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.dto.request; 2 | 3 | import org.terning.terningserver.user.domain.AuthType; 4 | 5 | public record SignUpRequest(String name, String profileImage, AuthType authType, String fcmToken) { 6 | } 7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/dto/request/SignUpFilterRequest.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.dto.request; 2 | 3 | public record SignUpFilterRequest( 4 | String grade, 5 | String workingPeriod, 6 | int startYear, 7 | int startMonth 8 | ) { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/banner/application/BannerService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.banner.application; 2 | 3 | import org.terning.terningserver.banner.dto.response.BannerListResponseDto; 4 | 5 | public interface BannerService { 6 | 7 | public BannerListResponseDto getBanners(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/application/port/ScrapSyncMarker.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.application.port; 2 | 3 | import java.util.List; 4 | 5 | public interface ScrapSyncMarker { 6 | void markAsSynced(List userIds); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/application/port/ScrapSyncNotifier.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.application.port; 2 | 3 | import java.util.List; 4 | 5 | public interface ScrapSyncNotifier { 6 | void notify(List userIds); 7 | } 8 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | # 📄 Work Description 3 | - 설명 4 | 5 | # ⚙️ ISSUE 6 | - closed #이슈번호 7 | 8 | 9 | # 📷 Screenshot 10 | - 동영상, 사진, 로그 등등 11 | - ex) 큐알 성공 이미지, 스웨거, 포스트맨 등 12 | 13 | 14 | # 💬 To Reviewers 15 | 리뷰어들에게 하고 싶은 말 16 | 17 | 18 | # 🔗 Reference 19 | 문제를 해결하면서 도움이 되었거나, 참고했던 사이트(코드링크) 20 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/application/port/UnsyncedScrapMarker.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.application.port; 2 | 3 | import java.util.List; 4 | 5 | public interface UnsyncedScrapMarker { 6 | void markAsSynced(List userIds); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/application/port/UnsyncedScrapUserReader.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.application.port; 2 | 3 | import java.util.List; 4 | 5 | public interface UnsyncedScrapUserReader { 6 | List readUnsyncedUserIds(); 7 | } 8 | 9 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/application/social/SocialAuthProvider.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.application.social; 2 | 3 | import org.terning.terningserver.user.domain.AuthType; 4 | 5 | public interface SocialAuthProvider { 6 | String getAuthId(String authAccessToken); 7 | AuthType supports(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/dto/request/PushStatusUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.dto.request; 2 | 3 | public record PushStatusUpdateRequest(String newStatus) { 4 | public static PushStatusUpdateRequest of(String newStatus) { 5 | return new PushStatusUpdateRequest(newStatus); 6 | } 7 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/💡-issue-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4A1 Issue Template" 3 | about: "\U0001F449 이슈 템플릿은 요거를 사용해주세요" 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | --- 11 | labels: [소셜로그인], [로깅] 12 | --- 13 | 14 | ## ⚙️ ISSUE 15 | - 어떤 이슈인지 설명 16 | 17 | 18 | ## 📄 To-Do 19 | - [ ] 이슈에서 세부 todo list 작성 20 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/user/application/UserSyncEventService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.user.application; 2 | 3 | 4 | public interface UserSyncEventService { 5 | 6 | void recordNameChange(Long userId, String newName); 7 | void recordWithdraw(Long userId); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/filter/dto/request/UpdateUserFilterRequestDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.filter.dto.request; 2 | 3 | public record UpdateUserFilterRequestDto( 4 | String jobType, 5 | String grade, 6 | String workingPeriod, 7 | int startYear, 8 | int startMonth 9 | ) { 10 | } 11 | -------------------------------------------------------------------------------- /src/test/java/org/terning/terningserver/TerningserverApplicationTests.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | 6 | @SpringBootTest 7 | class TerningserverApplicationTests { 8 | 9 | @Test 10 | void contextLoads() { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/dto/response/UnsyncedScrapUserResponse.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.dto.response; 2 | 3 | public record UnsyncedScrapUserResponse(Long userId) { 4 | public static UnsyncedScrapUserResponse from(Long userId) { 5 | return new UnsyncedScrapUserResponse(userId); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/event/UserSignedUpEvent.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.event; 2 | 3 | import org.terning.terningserver.user.domain.User; 4 | 5 | public record UserSignedUpEvent(User user, String fcmToken) { 6 | 7 | public static UserSignedUpEvent of(User user, String fcmToken) { 8 | return new UserSignedUpEvent(user, fcmToken); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/config/Login.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.config; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.PARAMETER) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface Login { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/dto/FcmTokenReissueRequiredResponse.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.dto; 2 | 3 | public record FcmTokenReissueRequiredResponse(boolean reissueRequired) { 4 | public static FcmTokenReissueRequiredResponse of(boolean reissueRequired) { 5 | return new FcmTokenReissueRequiredResponse(reissueRequired); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/filter/repository/FilterRepository.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.filter.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.springframework.stereotype.Repository; 5 | import org.terning.terningserver.filter.domain.Filter; 6 | 7 | @Repository 8 | public interface FilterRepository extends JpaRepository { 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/home/application/HomeService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.home.application; 2 | 3 | import org.springframework.data.domain.Pageable; 4 | import org.terning.terningserver.home.dto.response.HomeAnnouncementsResponseDto; 5 | 6 | public interface HomeService { 7 | 8 | HomeAnnouncementsResponseDto getAnnouncements(Long userId, String sortBy, Pageable pageable); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/banner/repository/BannerRepository.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.banner.repository; 2 | 3 | import org.springframework.data.repository.Repository; 4 | import org.terning.terningserver.banner.domain.Banner; 5 | 6 | import java.util.List; 7 | 8 | public interface BannerRepository extends Repository { 9 | 10 | List findAllByOrderByPriority(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/application/usecase/ReadUnsyncedScrapUsersUseCase.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.application.usecase; 2 | 3 | import org.terning.terningserver.external.pushNotification.scrap.dto.response.UnsyncedScrapUsersResponse; 4 | 5 | public interface ReadUnsyncedScrapUsersUseCase { 6 | UnsyncedScrapUsersResponse read(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/dto/request/ScrapUserIdsRequest.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.dto.request; 2 | 3 | import java.util.List; 4 | 5 | public record ScrapUserIdsRequest(List userIds) { 6 | public static ScrapUserIdsRequest of(List userIds) { 7 | return new ScrapUserIdsRequest(userIds); 8 | } 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/util/LogExecutionTime.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.util; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target({ElementType.METHOD}) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface LogExecutionTime { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/jwt/exception/JwtException.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.jwt.exception; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class JwtException extends RuntimeException { 7 | private final JwtErrorCode errorCode; 8 | 9 | public JwtException(JwtErrorCode errorCode) { 10 | super(errorCode.getMessage()); 11 | this.errorCode = errorCode; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/internshipAnnouncement/application/InternshipDetailService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.internshipAnnouncement.application; 2 | 3 | import org.terning.terningserver.internshipAnnouncement.dto.response.InternshipDetailResponseDto; 4 | 5 | 6 | public interface InternshipDetailService { 7 | 8 | InternshipDetailResponseDto getInternshipDetail(long internshipAnnouncementId, long userId); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/common/exception/AuthException.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.common.exception; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class AuthException extends RuntimeException { 7 | private final AuthErrorCode errorCode; 8 | 9 | public AuthException(AuthErrorCode errorCode) { 10 | super(errorCode.getMessage()); 11 | this.errorCode = errorCode; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/dto/request/SignInRequest.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.dto.request; 2 | 3 | import lombok.NonNull; 4 | import org.terning.terningserver.user.domain.AuthType; 5 | 6 | public record SignInRequest(@NonNull AuthType authType, String fcmToken) { 7 | public static SignInRequest of(AuthType authType, String fcmToken){ 8 | return new SignInRequest(authType, fcmToken); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/application/usecase/ScrapSyncOrchestrator.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.application.usecase; 2 | 3 | import org.terning.terningserver.external.pushNotification.scrap.dto.response.UnsyncedScrapUsersResponse; 4 | 5 | public interface ScrapSyncOrchestrator { 6 | UnsyncedScrapUsersResponse readUnsyncedUsers(); 7 | void syncUnsyncedUsers(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/config/RestTemplateConfig.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.client.RestTemplate; 6 | 7 | @Configuration 8 | public class RestTemplateConfig { 9 | 10 | @Bean 11 | public RestTemplate restTemplate() { 12 | return new RestTemplate(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/filter/application/FilterService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.filter.application; 2 | 3 | import org.terning.terningserver.filter.dto.request.UpdateUserFilterRequestDto; 4 | import org.terning.terningserver.filter.dto.response.UserFilterDetailResponseDto; 5 | 6 | public interface FilterService { 7 | 8 | UserFilterDetailResponseDto getUserFilter(Long userId); 9 | 10 | void updateUserFilter(UpdateUserFilterRequestDto responseDto, Long userId); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/user/respository/UserSyncEventRepository.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.user.respository; 2 | 3 | import java.util.List; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.terning.terningserver.external.pushNotification.user.domain.UserSyncEvent; 6 | 7 | public interface UserSyncEventRepository extends JpaRepository { 8 | List findByProcessedFalse(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/exception/CustomException.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.exception; 2 | 3 | import lombok.Getter; 4 | import org.terning.terningserver.common.exception.enums.ErrorMessage; 5 | 6 | @Getter 7 | public class CustomException extends RuntimeException { 8 | 9 | private ErrorMessage errorMessage; 10 | 11 | public CustomException(ErrorMessage errorMessage) { 12 | super(errorMessage.getMessage()); 13 | this.errorMessage = errorMessage; 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/common/failure/ScrapExternalException.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.common.failure; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class ScrapExternalException extends RuntimeException { 7 | 8 | private final ScrapExternalErrorCode errorCode; 9 | 10 | public ScrapExternalException(ScrapExternalErrorCode errorCode) { 11 | super(errorCode.getMessage()); 12 | this.errorCode = errorCode; 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/dto/request/CreateUserRequest.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.dto.request; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import org.terning.terningserver.user.domain.AuthType; 5 | 6 | public record CreateUserRequest( 7 | @JsonProperty("oUserId") 8 | Long userId, 9 | String name, 10 | AuthType authType, 11 | String fcmToken, 12 | String pushStatus, 13 | String accountStatus 14 | ) { 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/domain/Token.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.domain; 2 | 3 | import lombok.Builder; 4 | import lombok.EqualsAndHashCode; 5 | import lombok.Getter; 6 | 7 | @Getter 8 | @EqualsAndHashCode 9 | public class Token { 10 | 11 | private final String accessToken; 12 | private final String refreshToken; 13 | 14 | @Builder 15 | public Token(String accessToken, String refreshToken) { 16 | this.accessToken = accessToken; 17 | this.refreshToken = refreshToken; 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/common/success/AuthSuccessCode.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.common.success; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import org.springframework.http.HttpStatus; 6 | import org.terning.terningserver.common.exception.SuccessCode; 7 | 8 | @Getter 9 | @AllArgsConstructor 10 | public enum AuthSuccessCode implements SuccessCode { 11 | SUCCESS_SIGN_IN(HttpStatus.OK, "로그인에 성공했습니다."), 12 | ; 13 | 14 | private final HttpStatus status; 15 | private final String message; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/application/UserService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.application; 2 | 3 | import org.terning.terningserver.user.domain.User; 4 | import org.terning.terningserver.user.dto.request.ProfileUpdateRequestDto; 5 | import org.terning.terningserver.user.dto.response.ProfileResponseDto; 6 | 7 | public interface UserService { 8 | void deleteUser(User user); 9 | ProfileResponseDto getProfile(Long userId); 10 | 11 | void updateProfile(Long userId, ProfileUpdateRequestDto request); 12 | void updatePushStatus(Long userId, String newStatus); 13 | } 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/💡-issue-template-Feat.yml: -------------------------------------------------------------------------------- 1 | name: "✨ Feat" 2 | description: "새로운 기능 추가" 3 | labels: ["feat"] 4 | body: 5 | - type: textarea 6 | attributes: 7 | label: 📄 설명 8 | description: 새로운 기능에 대한 설명을 작성해 주세요. 9 | placeholder: 자세히 적을수록 좋습니다! 10 | validations: 11 | required: true 12 | - type: textarea 13 | attributes: 14 | label: ✅ 작업할 내용 15 | description: 할 일을 체크박스 형태로 작성해주세요. 16 | placeholder: 최대한 세분화 해서 적어주세요! 17 | validations: 18 | required: true 19 | - type: textarea 20 | attributes: 21 | label: 🙋🏻 참고 자료 22 | description: 참고 자료가 있다면 작성해 주세요. 23 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/dto/response/UnsyncedScrapUsersResponse.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.dto.response; 2 | 3 | import java.util.List; 4 | 5 | public record UnsyncedScrapUsersResponse(List users) { 6 | 7 | public static UnsyncedScrapUsersResponse of(List userIds) { 8 | List responses = userIds.stream() 9 | .map(UnsyncedScrapUserResponse::from) 10 | .toList(); 11 | 12 | return new UnsyncedScrapUsersResponse(responses); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/TerningserverApplication.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 6 | import org.springframework.scheduling.annotation.EnableScheduling; 7 | 8 | @EnableJpaAuditing 9 | @SpringBootApplication 10 | @EnableScheduling 11 | public class TerningserverApplication { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(TerningserverApplication.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/dto/request/ProfileUpdateRequestDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.dto.request; 2 | 3 | import lombok.Builder; 4 | import lombok.NonNull; 5 | 6 | import static lombok.AccessLevel.PRIVATE; 7 | 8 | @Builder(access = PRIVATE) 9 | public record ProfileUpdateRequestDto( 10 | @NonNull String name, 11 | String profileImage 12 | ) { 13 | public static ProfileUpdateRequestDto of(String name, String profileImage){ 14 | return ProfileUpdateRequestDto.builder() 15 | .name(name) 16 | .profileImage(profileImage) 17 | .build(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/config/QuerydslConfig.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.config; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | import jakarta.persistence.EntityManager; 5 | import jakarta.persistence.PersistenceContext; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | @Configuration 10 | public class QuerydslConfig { 11 | 12 | @PersistenceContext 13 | private EntityManager entityManager; 14 | 15 | @Bean 16 | public JPAQueryFactory jpaQueryFactory(){ 17 | return new JPAQueryFactory(entityManager); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/banner/api/BannerSwagger.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.banner.api; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.tags.Tag; 5 | 6 | import org.springframework.http.ResponseEntity; 7 | 8 | import org.terning.terningserver.banner.dto.response.BannerListResponseDto; 9 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 10 | 11 | @Tag(name= "Banner", description = "탐색 > 배너 조회 관련 API") 12 | public interface BannerSwagger { 13 | @Operation(summary = "배너 조회", description = "탐색 > 배너를 조회하는 API") 14 | ResponseEntity> getBanners(); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/search/application/SearchService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.search.application; 2 | 3 | import org.terning.terningserver.search.dto.response.PopularAnnouncementListResponseDto; 4 | import org.springframework.data.domain.Pageable; 5 | import org.terning.terningserver.search.dto.response.SearchResultResponseDto; 6 | 7 | public interface SearchService { 8 | 9 | PopularAnnouncementListResponseDto getMostViewedAnnouncements(); 10 | 11 | PopularAnnouncementListResponseDto getMostScrappedAnnouncements(); 12 | 13 | SearchResultResponseDto searchInternshipAnnouncement(String keyword, String sortBy, Pageable pageable, Long userId); 14 | 15 | } -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/dto/response/SignUpResponse.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.dto.response; 2 | 3 | import org.terning.terningserver.auth.dto.Token; 4 | import org.terning.terningserver.user.domain.AuthType; 5 | import org.terning.terningserver.user.domain.User; 6 | 7 | public record SignUpResponse(String accessToken, String refreshToken, Long userId, AuthType authType) { 8 | public static SignUpResponse of(Token token, User user) { 9 | return new SignUpResponse( 10 | token.accessToken(), 11 | token.refreshToken(), 12 | user.getId(), 13 | user.getAuthType() 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/main/resources/logback-local.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss,Asia/Seoul} [%thread] %highlight(%-5level) %cyan(%logger{36}) %green([%X{traceId:-NoTraceID}]) - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/internshipAnnouncement/repository/InternshipRepository.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.internshipAnnouncement.repository; 2 | 3 | import jakarta.persistence.LockModeType; 4 | 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.Lock; 7 | import org.terning.terningserver.internshipAnnouncement.domain.InternshipAnnouncement; 8 | 9 | import java.util.Optional; 10 | 11 | public interface InternshipRepository extends JpaRepository, InternshipRepositoryCustom { 12 | 13 | @Lock(LockModeType.PESSIMISTIC_WRITE) 14 | Optional findById(Long aLong); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.terning.terningserver.user.domain.User; 5 | import org.terning.terningserver.user.domain.AuthType; 6 | 7 | import java.util.Optional; 8 | 9 | public interface UserRepository extends JpaRepository { 10 | 11 | Optional findByRefreshToken(String refreshToken); 12 | 13 | Optional findByAuthId(String authId); 14 | 15 | Optional findByAuthIdAndAuthType(String authId, AuthType authType); 16 | 17 | boolean existsByAuthIdAndAuthType(String authId, AuthType authType); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/common/exception/AuthErrorCode.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.common.exception; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | public enum AuthErrorCode { 10 | 11 | USER_ALREADY_EXIST(HttpStatus.BAD_REQUEST, "유저가 이미 존재합니다."), 12 | USER_NOT_FOUND(HttpStatus.NOT_FOUND, "유저를 찾을 수 없습니다."), 13 | ; 14 | public static final String PREFIX = "[AUTH ERROR]"; 15 | 16 | private final HttpStatus status; 17 | private final String rawMessage; 18 | 19 | public String getMessage() { 20 | return PREFIX + " " + rawMessage; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/banner/domain/Banner.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.banner.domain; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.GeneratedValue; 5 | import jakarta.persistence.Id; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | 9 | import static jakarta.persistence.GenerationType.IDENTITY; 10 | import static lombok.AccessLevel.PROTECTED; 11 | 12 | @Entity 13 | @Getter 14 | @NoArgsConstructor(access = PROTECTED) 15 | public class Banner { 16 | 17 | @Id 18 | @GeneratedValue(strategy = IDENTITY) 19 | private Long id; 20 | 21 | private String imageUrl; 22 | 23 | private String link; 24 | 25 | private int priority; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/config/AuthServiceConfig.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.terning.terningserver.auth.application.social.SocialAuthProvider; 6 | import org.terning.terningserver.auth.application.social.SocialAuthServiceManager; 7 | 8 | import java.util.List; 9 | 10 | @Configuration 11 | public class AuthServiceConfig { 12 | 13 | @Bean 14 | public SocialAuthServiceManager socialAuthServiceFactory(List socialAuthProviders) { 15 | return SocialAuthServiceManager.create(socialAuthProviders); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/scrap/repository/ScrapRepository.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.scrap.repository; 2 | 3 | import org.springframework.data.jpa.repository.JpaRepository; 4 | import org.terning.terningserver.scrap.domain.Scrap; 5 | 6 | import java.util.Optional; 7 | 8 | public interface ScrapRepository extends JpaRepository, ScrapRepositoryCustom { 9 | Boolean existsByInternshipAnnouncementIdAndUserId(Long internshipId, Long userId); 10 | 11 | Optional findByInternshipAnnouncementIdAndUserId(Long internshipId, Long userId); 12 | 13 | void deleteByInternshipAnnouncementIdAndUserId(Long internshipId, Long userId); 14 | 15 | boolean existsByUserId(Long userId); 16 | } 17 | 18 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/scheduler/ScrapSyncScheduler.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.scheduler; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.scheduling.annotation.Scheduled; 5 | import org.springframework.stereotype.Component; 6 | import org.terning.terningserver.external.pushNotification.scrap.application.service.ScrapSyncCoordinatorService; 7 | 8 | @Component 9 | @RequiredArgsConstructor 10 | public class ScrapSyncScheduler { 11 | 12 | private final ScrapSyncCoordinatorService scrapSyncCoordinatorService; 13 | 14 | @Scheduled(cron = "0 30 21 * * *") 15 | public void syncScraps() { 16 | scrapSyncCoordinatorService.syncUnsyncedUsers(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/domain/ProfileImage.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.domain; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @RequiredArgsConstructor 7 | @Getter 8 | public enum ProfileImage { 9 | BASIC("basic"), 10 | LUCKY("lucky"), 11 | SMART("smart"), 12 | GLASS("glass"), 13 | CALENDAR("calendar"), 14 | PASSION("passion"); 15 | 16 | private final String value; 17 | 18 | public static ProfileImage fromValue(String value){ 19 | for(ProfileImage image : values()){ 20 | if(image.value.equalsIgnoreCase(value)){ 21 | return image; 22 | } 23 | } 24 | throw new IllegalArgumentException("Invalid profile image: " + value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/dto/response/ProfileResponseDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.dto.response; 2 | 3 | import lombok.Builder; 4 | import org.terning.terningserver.user.domain.User; 5 | 6 | @Builder 7 | public record ProfileResponseDto( 8 | String name, 9 | String profileImage, 10 | String authType, 11 | String pushStatus 12 | ) { 13 | public static ProfileResponseDto of(final User user){ 14 | return ProfileResponseDto.builder() 15 | .name(user.getName()) 16 | .profileImage(user.getProfileImage().getValue()) 17 | .authType(user.getAuthType().name().toUpperCase()) 18 | .pushStatus(user.getPushStatus().value().toUpperCase()) 19 | .build(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/common/failure/ScrapExternalErrorCode.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.common.failure; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | public enum ScrapExternalErrorCode { 10 | FAILED_TO_SEND_NOTIFICATION(HttpStatus.INTERNAL_SERVER_ERROR, "알림 서버 전송에 실패했습니다."), 11 | EMPTY_USER_IDS(HttpStatus.BAD_REQUEST, "전송할 유저 ID가 비어 있습니다."); 12 | 13 | public static final String PREFIX = "[SCRAP EXTERNAL ERROR]"; 14 | 15 | private final HttpStatus status; 16 | private final String rawMessage; 17 | 18 | public String getMessage() { 19 | return PREFIX + " " + rawMessage; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/scrap/domain/SyncStatus.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.scrap.domain; 2 | 3 | import jakarta.persistence.Embeddable; 4 | import lombok.EqualsAndHashCode; 5 | 6 | @Embeddable 7 | @EqualsAndHashCode 8 | public class SyncStatus { 9 | 10 | private boolean value; 11 | 12 | protected SyncStatus() { 13 | } 14 | 15 | private SyncStatus(boolean value) { 16 | this.value = value; 17 | } 18 | 19 | public static SyncStatus notSynced() { 20 | return new SyncStatus(false); 21 | } 22 | 23 | public static SyncStatus synced() { 24 | return new SyncStatus(true); 25 | } 26 | 27 | public boolean isSynced() { 28 | return value; 29 | } 30 | 31 | public boolean isNotSynced() { 32 | return !value; 33 | } 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/application/social/apple/AppleAuthProvider.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.application.social.apple; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | import org.terning.terningserver.auth.application.social.SocialAuthProvider; 6 | import org.terning.terningserver.user.domain.AuthType; 7 | 8 | @Service 9 | @RequiredArgsConstructor 10 | public class AppleAuthProvider implements SocialAuthProvider { 11 | 12 | private final AppleAuthTokenValidator appleAuthTokenValidator; 13 | 14 | @Override 15 | public String getAuthId(String authAccessToken) { 16 | return appleAuthTokenValidator.extractAppleId(authAccessToken); 17 | } 18 | 19 | @Override 20 | public AuthType supports() { 21 | return AuthType.APPLE; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/application/social/kakao/KakaoAuthProvider.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.application.social.kakao; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | import org.terning.terningserver.auth.application.social.SocialAuthProvider; 6 | import org.terning.terningserver.user.domain.AuthType; 7 | 8 | @Service 9 | @RequiredArgsConstructor 10 | public class KakaoAuthProvider implements SocialAuthProvider { 11 | 12 | private final KakaoAuthTokenValidator kakaoAuthTokenValidator; 13 | 14 | @Override 15 | public String getAuthId(String authAccessToken) { 16 | return kakaoAuthTokenValidator.extractKakaoId(authAccessToken); 17 | } 18 | 19 | @Override 20 | public AuthType supports() { 21 | return AuthType.KAKAO; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/banner/application/BannerServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.banner.application; 2 | 3 | import org.springframework.stereotype.Service; 4 | import org.terning.terningserver.banner.domain.Banner; 5 | import org.terning.terningserver.banner.dto.response.BannerListResponseDto; 6 | import org.terning.terningserver.banner.repository.BannerRepository; 7 | 8 | import lombok.RequiredArgsConstructor; 9 | 10 | import java.util.List; 11 | 12 | 13 | @Service 14 | @RequiredArgsConstructor 15 | public class BannerServiceImpl implements BannerService { 16 | 17 | private final BannerRepository bannerRepository; 18 | 19 | public BannerListResponseDto getBanners() { 20 | List banners = bannerRepository.findAllByOrderByPriority(); 21 | return BannerListResponseDto.of(banners); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/internshipAnnouncement/domain/Company.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.internshipAnnouncement.domain; 2 | 3 | 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Embeddable; 6 | import jakarta.persistence.EnumType; 7 | import jakarta.persistence.Enumerated; 8 | import lombok.Getter; 9 | import lombok.NoArgsConstructor; 10 | 11 | import static lombok.AccessLevel.PROTECTED; 12 | 13 | @Embeddable 14 | @Getter 15 | @NoArgsConstructor(access = PROTECTED) 16 | public class Company { 17 | 18 | @Column(nullable = false, length = 64) 19 | private String companyInfo; // 기업명 20 | 21 | @Enumerated(EnumType.STRING) 22 | @Column(nullable = false) 23 | private CompanyCategory companyCategory; // 회사 카테고리 24 | 25 | @Column(nullable = false, length = 256) 26 | private String companyImage; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/application/service/UnsyncedScrapUserReaderService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.application.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | import org.terning.terningserver.external.pushNotification.scrap.application.port.UnsyncedScrapUserReader; 6 | import org.terning.terningserver.scrap.repository.ScrapRepository; 7 | 8 | import java.util.List; 9 | 10 | @Service 11 | @RequiredArgsConstructor 12 | public class UnsyncedScrapUserReaderService implements UnsyncedScrapUserReader { 13 | 14 | private final ScrapRepository scrapRepository; 15 | 16 | @Override 17 | public List readUnsyncedUserIds() { 18 | return scrapRepository.findUserIdsWithUnsyncedScraps(); 19 | } 20 | } 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/banner/dto/response/BannerListResponseDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.banner.dto.response; 2 | 3 | import org.terning.terningserver.banner.domain.Banner; 4 | 5 | import java.util.List; 6 | 7 | public record BannerListResponseDto( 8 | List banners 9 | ) { 10 | 11 | public record BannerDetailResponseDto( 12 | String imageUrl, 13 | String link 14 | ) { 15 | public static BannerDetailResponseDto of(final Banner banner) { 16 | return new BannerDetailResponseDto(banner.getImageUrl(), banner.getLink()); 17 | } 18 | } 19 | 20 | public static BannerListResponseDto of(final List banners) { 21 | return new BannerListResponseDto( 22 | banners.stream().map(BannerDetailResponseDto::of).toList() 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/BaseTimeEntity.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.EntityListeners; 5 | import jakarta.persistence.MappedSuperclass; 6 | import lombok.Getter; 7 | import org.springframework.data.annotation.CreatedDate; 8 | import org.springframework.data.annotation.LastModifiedDate; 9 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 10 | 11 | import java.time.LocalDateTime; 12 | 13 | @Getter 14 | @EntityListeners(AuditingEntityListener.class) 15 | @MappedSuperclass 16 | public abstract class BaseTimeEntity { 17 | 18 | @CreatedDate 19 | @Column(name = "created_at", nullable = false) 20 | private LocalDateTime createdAt; 21 | 22 | @LastModifiedDate 23 | @Column(name = "modified_at", nullable = false) 24 | private LocalDateTime modifiedAt; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/home/dto/response/HomeAnnouncementsResponseDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.home.dto.response; 2 | 3 | import lombok.Builder; 4 | 5 | import java.util.List; 6 | 7 | @Builder 8 | public record HomeAnnouncementsResponseDto( 9 | int totalPages, 10 | long totalCount, 11 | boolean hasNext, 12 | List result 13 | ) { 14 | public static HomeAnnouncementsResponseDto of( 15 | final int totalPages, 16 | final long totalCount, 17 | final boolean hasNext, 18 | final List announcements) { 19 | return HomeAnnouncementsResponseDto.builder() 20 | .totalPages(totalPages) 21 | .totalCount(totalCount) 22 | .hasNext(hasNext) 23 | .result(announcements) 24 | .build(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/internshipAnnouncement/repository/InternshipRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.internshipAnnouncement.repository; 2 | 3 | import com.querydsl.core.Tuple; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.terning.terningserver.internshipAnnouncement.domain.InternshipAnnouncement; 7 | import org.terning.terningserver.user.domain.User; 8 | 9 | import java.util.List; 10 | 11 | public interface InternshipRepositoryCustom { 12 | List getMostViewedInternship(); 13 | 14 | List getMostScrappedInternship(); 15 | 16 | Page searchInternshipAnnouncement(String keyword, String sortBy, Pageable pageable); 17 | 18 | Page findFilteredInternshipsWithScrapInfo(User user, String sortBy, Pageable pageable); 19 | 20 | Page findAllInternshipsWithScrapInfo(User user, String sortBy, Pageable pageable); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/internshipAnnouncement/api/InternshipDetailSwagger.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.internshipAnnouncement.api; 2 | 3 | 4 | import io.swagger.v3.oas.annotations.Operation; 5 | import io.swagger.v3.oas.annotations.tags.Tag; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.terning.terningserver.auth.config.Login; 9 | import org.terning.terningserver.internshipAnnouncement.dto.response.InternshipDetailResponseDto; 10 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 11 | 12 | @Tag(name = "InternshipDetail", description = "공고 상세 페이지 관련 API") 13 | public interface InternshipDetailSwagger { 14 | 15 | @Operation(summary = "공고 상세 페이지", description = "인턴 공고의 상세 정보를 불러오는 API") 16 | ResponseEntity> getInternshipDetail( 17 | @Login Long userId, 18 | @PathVariable("internshipAnnouncementId") Long internshipAnnouncementId 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/config/ValueConfig.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.config; 2 | 3 | import jakarta.annotation.PostConstruct; 4 | import lombok.Getter; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | import java.nio.charset.StandardCharsets; 9 | import java.util.Base64; 10 | 11 | @Configuration 12 | @Getter 13 | public class ValueConfig { 14 | 15 | @Value("${jwt.secret-key}") 16 | private String secretKey; 17 | 18 | @Value("${jwt.kakao-url}") 19 | private String kakaoUri; 20 | 21 | @Value("${jwt.apple-url}") 22 | private String appleUri; 23 | 24 | @Value("${jwt.access-token-expired}") 25 | private Long accessTokenExpired; 26 | 27 | @Value("${jwt.refresh-token-expired}") 28 | private Long refreshTokenExpired; 29 | 30 | @PostConstruct 31 | protected void init() { 32 | secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/ScrapSyncMarkerImpl.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.transaction.annotation.Transactional; 6 | import org.terning.terningserver.scrap.domain.Scrap; 7 | import org.terning.terningserver.external.pushNotification.scrap.application.port.ScrapSyncMarker; 8 | import org.terning.terningserver.scrap.repository.ScrapRepository; 9 | 10 | import java.util.List; 11 | 12 | @Service 13 | @RequiredArgsConstructor 14 | public class ScrapSyncMarkerImpl implements ScrapSyncMarker { 15 | 16 | private final ScrapRepository scrapRepository; 17 | 18 | @Override 19 | @Transactional 20 | public void markAsSynced(List userIds) { 21 | if (userIds == null || userIds.isEmpty()) return; 22 | 23 | scrapRepository.findUnsyncedScrapsByUserIds(userIds) 24 | .forEach(Scrap::markSynced); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/dto/response/SignInResponse.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.dto.response; 2 | 3 | import org.terning.terningserver.auth.dto.Token; 4 | import org.terning.terningserver.user.domain.AuthType; 5 | 6 | public record SignInResponse( 7 | String accessToken, 8 | String refreshToken, 9 | String authId, 10 | AuthType authType, 11 | Long userId 12 | ) { 13 | public static SignInResponse ofExistingUser(Token token, String authId, AuthType authType, Long userId) { 14 | return new SignInResponse( 15 | token.accessToken(), 16 | token.refreshToken(), 17 | authId, 18 | authType, 19 | userId 20 | ); 21 | } 22 | 23 | public static SignInResponse ofNewUser(String authId, AuthType authType) { 24 | return new SignInResponse( 25 | null, 26 | null, 27 | authId, 28 | authType, 29 | null 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/application/service/ReadUnsyncedScrapUsersService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.application.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | import org.terning.terningserver.external.pushNotification.scrap.application.port.UnsyncedScrapUserReader; 6 | import org.terning.terningserver.external.pushNotification.scrap.application.usecase.ReadUnsyncedScrapUsersUseCase; 7 | import org.terning.terningserver.external.pushNotification.scrap.dto.response.UnsyncedScrapUsersResponse; 8 | 9 | import java.util.List; 10 | 11 | @Service 12 | @RequiredArgsConstructor 13 | public class ReadUnsyncedScrapUsersService implements ReadUnsyncedScrapUsersUseCase { 14 | 15 | private final UnsyncedScrapUserReader reader; 16 | 17 | @Override 18 | public UnsyncedScrapUsersResponse read() { 19 | List userIds = reader.readUnsyncedUserIds(); 20 | return UnsyncedScrapUsersResponse.of(userIds); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/jwt/exception/JwtErrorCode.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.jwt.exception; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | public enum JwtErrorCode { 10 | 11 | INVALID_USER_ID_TYPE(HttpStatus.BAD_REQUEST, "사용자 ID의 타입이 유효하지 않습니다."), 12 | EMPTY_TOKEN(HttpStatus.BAD_REQUEST, "토큰이 비어있거나 유효하지 않은 형식입니다."), 13 | 14 | TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "HTTP Authorization 헤더를 찾을 수 없습니다."), 15 | EXPIRED_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."), 16 | MALFORMED_TOKEN(HttpStatus.UNAUTHORIZED, "잘못된 형식의 토큰입니다."), 17 | SIGNATURE_ERROR(HttpStatus.UNAUTHORIZED, "토큰 서명 검증에 실패했습니다."), 18 | UNSUPPORTED_TOKEN(HttpStatus.UNAUTHORIZED, "지원되지 않는 방식의 토큰입니다."), 19 | INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."), 20 | 21 | UNEXPECTED_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "토큰 처리 중 예상치 못한 서버 오류가 발생했습니다."); 22 | 23 | private final HttpStatus status; 24 | private final String message; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/filter/domain/Grade.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.filter.domain; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @Getter 7 | @RequiredArgsConstructor 8 | public enum Grade { 9 | FRESHMAN("freshman", "1학년"), 10 | SOPHOMORE("sophomore", "2학년"), 11 | JUNIOR("junior", "3학년"), 12 | SENIOR("senior", "4학년"); 13 | 14 | private final String key; 15 | private final String value; 16 | 17 | public static Grade fromKey(String key){ 18 | for(Grade grade : Grade.values()){ 19 | if(grade.key.equals(key)){ 20 | return grade; 21 | } 22 | } 23 | throw new IllegalArgumentException("올바르지 않은 요청 값입니다."); 24 | } 25 | 26 | public static Grade fromString(String value){ 27 | for(Grade grade : Grade.values()){ 28 | if(grade.value.equalsIgnoreCase(value)) { 29 | return grade; 30 | } 31 | } 32 | throw new IllegalArgumentException("올바르지 않은 요청 값입니다."); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/scrap/repository/ScrapRepositoryCustom.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.scrap.repository; 2 | 3 | import org.terning.terningserver.internshipAnnouncement.domain.InternshipAnnouncement; 4 | import org.terning.terningserver.scrap.domain.Scrap; 5 | 6 | import java.time.LocalDate; 7 | import java.util.List; 8 | 9 | public interface ScrapRepositoryCustom { 10 | Long findScrapIdByInternshipAnnouncementIdAndUserId(Long internshipAnnouncementId, Long userId); 11 | 12 | List findAllByInternshipAndUserId(List internshipAnnouncements, Long userId); 13 | 14 | List findScrapsByUserIdAndDeadlineBetweenOrderByDeadline(Long userId, LocalDate start, LocalDate end); 15 | 16 | List findScrapsByUserIdAndDeadlineOrderByDeadline(Long userId, LocalDate deadline); 17 | 18 | String findColorByInternshipAnnouncementIdAndUserId(Long internshipAnnouncementId, Long userId); 19 | 20 | List findUserIdsWithUnsyncedScraps(); 21 | 22 | List findUnsyncedScrapsByUserIds(List userIds); 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/filter/domain/JobType.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.filter.domain; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import org.terning.terningserver.common.exception.CustomException; 6 | import org.terning.terningserver.common.exception.enums.ErrorMessage; 7 | 8 | @Getter 9 | @RequiredArgsConstructor 10 | public enum JobType { 11 | TOTAL("total", "전체"), 12 | PLAN("plan", "기획/전략"), 13 | MARKETING("marketing", "마케팅/홍보"), 14 | ADMIN("admin", "사무/회계"), 15 | SALES("sales", "인사/영업"), 16 | DESIGN("design", "디자인/예술"), 17 | IT("it", "개발/IT"), 18 | RESEARCH("research", "연구/생산"), 19 | ETC("etc", "기타"); 20 | 21 | private final String key; 22 | private final String value; 23 | 24 | public static JobType fromKey(String key) { 25 | for (JobType jobType : JobType.values()) { 26 | if (jobType.key.equalsIgnoreCase(key)) { 27 | return jobType; 28 | } 29 | } 30 | throw new CustomException(ErrorMessage.INVALID_JOB_TYPE); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/calendar/dto/response/MonthlyDefaultResponseDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.calendar.dto.response; 2 | 3 | 4 | import lombok.Builder; 5 | 6 | import java.util.List; 7 | 8 | @Builder 9 | public record MonthlyDefaultResponseDto( 10 | String deadline, 11 | List scraps 12 | ) { 13 | @Builder 14 | public static record ScrapDetail( 15 | Long scrapId, 16 | String title, 17 | String color 18 | ){ 19 | public static ScrapDetail of(final Long scrapId, final String title, final String color){ 20 | return ScrapDetail.builder() 21 | .scrapId(scrapId) 22 | .title(title) 23 | .color(color) 24 | .build(); 25 | } 26 | } 27 | 28 | public static MonthlyDefaultResponseDto of(String deadline, List scraps){ 29 | return MonthlyDefaultResponseDto.builder() 30 | .deadline(deadline) 31 | .scraps(scraps) 32 | .build(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/application/service/UnsyncedScrapMarkerService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.application.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.transaction.annotation.Transactional; 6 | import org.terning.terningserver.external.pushNotification.scrap.application.port.UnsyncedScrapMarker; 7 | import org.terning.terningserver.scrap.domain.Scrap; 8 | import org.terning.terningserver.scrap.repository.ScrapRepository; 9 | 10 | import java.util.List; 11 | 12 | @Service 13 | @RequiredArgsConstructor 14 | public class UnsyncedScrapMarkerService implements UnsyncedScrapMarker { 15 | 16 | private final ScrapRepository scrapRepository; 17 | 18 | @Transactional 19 | @Override 20 | public void markAsSynced(List userIds) { 21 | if (userIds == null || userIds.isEmpty()) return; 22 | 23 | scrapRepository.findUnsyncedScrapsByUserIds(userIds) 24 | .forEach(Scrap::markSynced); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/filter/domain/WorkingPeriod.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.filter.domain; 2 | 3 | 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | 7 | @Getter 8 | @RequiredArgsConstructor 9 | public enum WorkingPeriod { 10 | OPTION1("short", "1개월 ~ 3개월"), 11 | OPTION2("middle", "4개월 ~ 6개월"), 12 | OPTION3("long", "7개월 이상"); 13 | 14 | private final String key; 15 | private final String value; 16 | 17 | public static WorkingPeriod fromKey(String key){ 18 | for(WorkingPeriod period : WorkingPeriod.values()){ 19 | if(period.key.equals(key)){ 20 | return period; 21 | } 22 | } 23 | throw new IllegalArgumentException("올바르지 않은 요청 값입니다."); 24 | } 25 | public static WorkingPeriod fromString(String value){ 26 | for(WorkingPeriod period : WorkingPeriod.values()){ 27 | if(period.value.equalsIgnoreCase(value)) { 28 | return period; 29 | } 30 | } 31 | throw new IllegalArgumentException("올바르지 않은 요청 값입니다."); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/util/DateUtil.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.util; 2 | 3 | import java.time.LocalDate; 4 | import java.time.Period; 5 | import java.time.ZoneId; 6 | import java.time.ZonedDateTime; 7 | import java.time.temporal.ChronoUnit; 8 | 9 | public class DateUtil { 10 | 11 | public static String convert(LocalDate deadline) { 12 | ZonedDateTime nowInKorea = ZonedDateTime.now(ZoneId.of("Asia/Seoul")); 13 | LocalDate currentDate = nowInKorea.toLocalDate(); 14 | 15 | if (deadline.isEqual(currentDate)) { 16 | return "D-DAY"; 17 | } else if (deadline.isBefore(currentDate)) { 18 | return "지원마감"; 19 | } else { 20 | long daysUntilDeadline = ChronoUnit.DAYS.between(currentDate, deadline); 21 | return "D-" + daysUntilDeadline; 22 | } 23 | } 24 | 25 | public static String convertDeadline(LocalDate deadline) { 26 | return deadline.getYear() + "년 " 27 | + deadline.getMonthValue() + "월 " 28 | + deadline.getDayOfMonth() + "일"; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/DEV-CI.yml: -------------------------------------------------------------------------------- 1 | name: DEV-CI 2 | 3 | on: 4 | pull_request: 5 | branches: [ "develop" ] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-24.04 10 | env: 11 | working-directory: . 12 | 13 | # Checkout - 가상 머신에 체크아웃 14 | steps: 15 | - name: 체크아웃 16 | uses: actions/checkout@v3 17 | 18 | # JDK setting - JDK 21 설정 19 | - name: Set up JDK 21 20 | uses: actions/setup-java@v3 21 | with: 22 | distribution: 'temurin' 23 | java-version: '21' 24 | 25 | # Gradle caching - 빌드 시간 향상 26 | - name: Gradle Caching 27 | uses: actions/cache@v3 28 | with: 29 | path: | 30 | ~/.gradle/caches 31 | ~/.gradle/wrapper 32 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 33 | restore-keys: | 34 | ${{ runner.os }}-gradle- 35 | 36 | # Gradle build - 테스트 없이 gradle 빌드 37 | - name: 빌드 38 | run: | 39 | chmod +x gradlew 40 | ./gradlew build -x test 41 | working-directory: ${{ env.working-directory }} 42 | shell: bash -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/api/ScrapExternalApiController.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.api; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.*; 6 | import org.terning.terningserver.external.pushNotification.scrap.application.usecase.ScrapSyncOrchestrator; 7 | import org.terning.terningserver.external.pushNotification.scrap.dto.response.UnsyncedScrapUsersResponse; 8 | 9 | @RestController 10 | @RequiredArgsConstructor 11 | @RequestMapping("/api/v1/external/scraps") 12 | public class ScrapExternalApiController { 13 | 14 | private final ScrapSyncOrchestrator scrapSyncOrchestrator; 15 | 16 | @GetMapping("/unsynced") 17 | public ResponseEntity fetchUnsyncedScrapUsers() { 18 | return ResponseEntity.ok(scrapSyncOrchestrator.readUnsyncedUsers()); 19 | } 20 | 21 | @PostMapping("/sync/result") 22 | public ResponseEntity syncScrapsManually() { 23 | scrapSyncOrchestrator.syncUnsyncedUsers(); 24 | return ResponseEntity.ok().build(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/home/api/HomeSwagger.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.home.api; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.tags.Tag; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.http.ResponseEntity; 7 | import org.terning.terningserver.home.dto.response.HomeAnnouncementsResponseDto; 8 | import org.terning.terningserver.home.dto.response.UpcomingScrapResponseDto; 9 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 10 | 11 | import java.util.List; 12 | 13 | @Tag(name = "Home", description = "홈화면 관련 API") 14 | public interface HomeSwagger { 15 | 16 | @Operation(summary = "홈화면 > 나에게 딱맞는 인턴 공고 조회", description = "특정 사용자에 필터링 조건에 맞는 인턴 공고 정보를 조회하는 API") 17 | ResponseEntity> getAnnouncements( 18 | Long userId, 19 | String sortBy, 20 | Pageable pageable 21 | ); 22 | 23 | @Operation(summary = "홈화면 > 곧 마감인 스크랩 공고 조회", description = "곧 마감인 스크랩 공고를 조회하는 API") 24 | ResponseEntity>> getUpcomingScraps( 25 | Long userId 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/banner/api/BannerController.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.banner.api; 2 | 3 | 4 | import lombok.RequiredArgsConstructor; 5 | 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import org.terning.terningserver.banner.dto.response.BannerListResponseDto; 12 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 13 | import org.terning.terningserver.common.exception.enums.SuccessMessage; 14 | import org.terning.terningserver.banner.application.BannerService; 15 | 16 | @RestController 17 | @RequiredArgsConstructor 18 | @RequestMapping("/api/v1") 19 | public class BannerController implements BannerSwagger{ 20 | 21 | private final BannerService bannerService; 22 | 23 | @GetMapping("/search/banners") 24 | public ResponseEntity> getBanners() { 25 | return ResponseEntity.ok( 26 | SuccessResponse.of(SuccessMessage.SUCCESS_GET_BANNERS, bannerService.getBanners()) 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/filter/dto/response/UserFilterDetailResponseDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.filter.dto.response; 2 | 3 | import lombok.Builder; 4 | import org.terning.terningserver.filter.domain.Filter; 5 | 6 | import static lombok.AccessLevel.PRIVATE; 7 | 8 | @Builder(access = PRIVATE) 9 | public record UserFilterDetailResponseDto( 10 | String jobType, 11 | String grade, 12 | String workingPeriod, 13 | Integer startYear, 14 | Integer startMonth 15 | ) { 16 | public static UserFilterDetailResponseDto of(final Filter userFilter) { 17 | return UserFilterDetailResponseDto.builder() 18 | .jobType(userFilter == null || userFilter.getJobType() == null ? "total" : userFilter.getJobType().getKey()) 19 | .grade(userFilter == null || userFilter.getGrade() == null ? null : userFilter.getGrade().getKey()) 20 | .workingPeriod(userFilter == null || userFilter.getWorkingPeriod() == null ? null : userFilter.getWorkingPeriod().getKey()) 21 | .startYear(userFilter == null ? null : userFilter.getStartYear()) 22 | .startMonth(userFilter == null ? null : userFilter.getStartMonth()) 23 | .build(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/internshipAnnouncement/domain/CompanyCategory.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.internshipAnnouncement.domain; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | 6 | @RequiredArgsConstructor 7 | @Getter 8 | public enum CompanyCategory { 9 | 10 | LARGE_AND_MEDIUM_COMPANIES(0, "대기업/중견기업"), 11 | SMALL_COMPANIES(1, "중소기업"), 12 | PUBLIC_INSTITUTIONS(2, "공공기관/공기업"), 13 | FOREIGN_COMPANIES(3, "외국계기업"), 14 | STARTUPS(4, "스타트업"), 15 | NON_PROFIT_ORGANIZATIONS(5, "비영리단체/재단"), 16 | OTHERS(6, "기타"); 17 | 18 | private final int key; 19 | private final String value; 20 | 21 | public static CompanyCategory fromKey(int key){ 22 | for(CompanyCategory category : CompanyCategory.values()){ 23 | if(category.key == key){ 24 | return category; 25 | } 26 | } 27 | throw new IllegalArgumentException("올바르지 않은 요청 값입니다."); 28 | } 29 | public static CompanyCategory fromString(String value){ 30 | for(CompanyCategory category : CompanyCategory.values()){ 31 | if(category.value.equalsIgnoreCase(value)) { 32 | return category; 33 | } 34 | } 35 | throw new IllegalArgumentException("올바르지 않은 요청 값입니다."); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/filter/api/FilterSwagger.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.filter.api; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.Parameter; 5 | import io.swagger.v3.oas.annotations.tags.Tag; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.terning.terningserver.auth.config.Login; 9 | import org.terning.terningserver.filter.dto.request.UpdateUserFilterRequestDto; 10 | import org.terning.terningserver.filter.dto.response.UserFilterDetailResponseDto; 11 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 12 | 13 | @Tag(name = "Filter", description = "사용자 필터링 관련 API") 14 | public interface FilterSwagger { 15 | 16 | @Operation(summary = "사용자 필터링 정보 조회 API", description = "사용자가 설정한 필터링 정보를 조회하는 API") 17 | ResponseEntity> getUserFilter( 18 | @Parameter(hidden = true) @Login long userId 19 | ); 20 | 21 | @Operation(summary = "사용자 필터링 정보 수정 API", description = "사용자 필터링을 수정하는 API") 22 | ResponseEntity updateUserFilter( 23 | @Parameter(hidden = true) @Login long userId, 24 | @RequestBody UpdateUserFilterRequestDto requestDto 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/application/social/SocialAuthServiceManager.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.application.social; 2 | 3 | import org.terning.terningserver.user.domain.AuthType; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.function.Function; 8 | import java.util.stream.Collectors; 9 | 10 | public class SocialAuthServiceManager { 11 | 12 | private final Map authServiceMap; 13 | 14 | private SocialAuthServiceManager(Map authServiceMap) { 15 | this.authServiceMap = authServiceMap; 16 | } 17 | 18 | public static SocialAuthServiceManager create(List socialAuthProviders) { 19 | Map authServiceMap = socialAuthProviders.stream() 20 | .collect(Collectors.toMap(SocialAuthProvider::supports, Function.identity())); 21 | return new SocialAuthServiceManager(authServiceMap); 22 | } 23 | 24 | public SocialAuthProvider getAuthService(AuthType authType) { 25 | SocialAuthProvider provider = authServiceMap.get(authType); 26 | if (provider == null) { 27 | throw new IllegalArgumentException("지원되지 않는 소셜 로그인 타입: " + authType); 28 | } 29 | return provider; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/util/LogAspect.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.util; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.aspectj.lang.ProceedingJoinPoint; 5 | import org.aspectj.lang.annotation.Around; 6 | import org.aspectj.lang.annotation.Aspect; 7 | import org.aspectj.lang.reflect.MethodSignature; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.util.StopWatch; 12 | 13 | @Component 14 | @Aspect 15 | @RequiredArgsConstructor 16 | public class LogAspect { 17 | 18 | Logger logger = LoggerFactory.getLogger(LogAspect.class); 19 | 20 | @Around("@annotation(org.terning.terningserver.common.util.LogExecutionTime)") 21 | public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { 22 | StopWatch stopWatch = new StopWatch(); 23 | stopWatch.start(); 24 | 25 | //@LogExecutionTime이 붙어있는 타켓 메소드의 성능 측정 26 | Object proceed = joinPoint.proceed(); 27 | 28 | stopWatch.stop(); 29 | 30 | MethodSignature signature = (MethodSignature) joinPoint.getSignature(); 31 | logger.info("실행한 메소드: " + signature.getMethod().getName()); 32 | logger.info("수행 시간: " + stopWatch.getTotalTimeMillis() + "ms"); 33 | 34 | return proceed; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/search/dto/response/PopularAnnouncementListResponseDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.search.dto.response; 2 | 3 | import lombok.Builder; 4 | import org.terning.terningserver.internshipAnnouncement.domain.InternshipAnnouncement; 5 | 6 | import java.util.List; 7 | 8 | public record PopularAnnouncementListResponseDto( 9 | List announcements 10 | ) { 11 | 12 | @Builder 13 | public record MostViewedAndScrappedAnnouncement( 14 | Long internshipAnnouncementId, 15 | String companyImage, 16 | String title 17 | ) { 18 | public static MostViewedAndScrappedAnnouncement from(InternshipAnnouncement announcement) { 19 | return MostViewedAndScrappedAnnouncement.builder() 20 | .internshipAnnouncementId(announcement.getId()) 21 | .companyImage(announcement.getCompany().getCompanyImage()) 22 | .title(announcement.getTitle()) 23 | .build(); 24 | } 25 | } 26 | 27 | public static PopularAnnouncementListResponseDto of(List announcements) { 28 | return new PopularAnnouncementListResponseDto( 29 | announcements.stream().map(MostViewedAndScrappedAnnouncement::from).toList() 30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/application/service/SyncUnsyncedUsersService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.application.service; 2 | 3 | import jakarta.transaction.Transactional; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Service; 6 | import org.terning.terningserver.external.pushNotification.scrap.application.port.ScrapSyncNotifier; 7 | import org.terning.terningserver.external.pushNotification.scrap.application.port.UnsyncedScrapMarker; 8 | import org.terning.terningserver.external.pushNotification.scrap.application.port.UnsyncedScrapUserReader; 9 | import org.terning.terningserver.external.pushNotification.scrap.application.usecase.SyncUnsyncedUsersUseCase; 10 | 11 | import java.util.List; 12 | 13 | @Service 14 | @RequiredArgsConstructor 15 | public class SyncUnsyncedUsersService implements SyncUnsyncedUsersUseCase { 16 | 17 | private final UnsyncedScrapUserReader reader; 18 | private final ScrapSyncNotifier notifier; 19 | private final UnsyncedScrapMarker marker; 20 | 21 | @Override 22 | @Transactional 23 | public void sync() { 24 | List userIds = reader.readUnsyncedUserIds(); 25 | if (userIds.isEmpty()) return; 26 | 27 | notifier.notify(userIds); 28 | marker.markAsSynced(userIds); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/application/service/ScrapSyncCoordinatorService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.application.service; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.transaction.annotation.Transactional; 6 | import org.terning.terningserver.external.pushNotification.scrap.application.usecase.ReadUnsyncedScrapUsersUseCase; 7 | import org.terning.terningserver.external.pushNotification.scrap.application.usecase.ScrapSyncOrchestrator; 8 | import org.terning.terningserver.external.pushNotification.scrap.application.usecase.SyncUnsyncedUsersUseCase; 9 | import org.terning.terningserver.external.pushNotification.scrap.dto.response.UnsyncedScrapUsersResponse; 10 | 11 | @Service 12 | @RequiredArgsConstructor 13 | @Transactional(readOnly = true) 14 | public class ScrapSyncCoordinatorService implements ScrapSyncOrchestrator { 15 | 16 | private final ReadUnsyncedScrapUsersUseCase readUseCase; 17 | private final SyncUnsyncedUsersUseCase syncUseCase; 18 | 19 | @Override 20 | public UnsyncedScrapUsersResponse readUnsyncedUsers() { 21 | return readUseCase.read(); 22 | } 23 | 24 | @Override 25 | @Transactional 26 | public void syncUnsyncedUsers() { 27 | syncUseCase.sync(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/scrap/application/ScrapService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.scrap.application; 2 | 3 | import org.terning.terningserver.scrap.dto.request.CreateScrapRequestDto; 4 | import org.terning.terningserver.scrap.dto.request.UpdateScrapRequestDto; 5 | import org.terning.terningserver.calendar.dto.response.DailyScrapResponseDto; 6 | import org.terning.terningserver.calendar.dto.response.MonthlyDefaultResponseDto; 7 | import org.terning.terningserver.calendar.dto.response.MonthlyListResponseDto; 8 | import org.terning.terningserver.home.dto.response.UpcomingScrapResponseDto; 9 | 10 | import java.time.LocalDate; 11 | import java.util.List; 12 | 13 | public interface ScrapService { 14 | 15 | boolean hasUserScrapped(long userId); 16 | List getUpcomingScrap(long userId); 17 | 18 | void createScrap(Long internshipAnnouncementId, CreateScrapRequestDto request, Long userId); 19 | 20 | void deleteScrap(Long scrapId, Long userId); 21 | 22 | void updateScrapColor(Long scrapId, UpdateScrapRequestDto request, Long userId); 23 | 24 | List getMonthlyScraps(Long userId, int year, int month); 25 | 26 | List getMonthlyScrapsAsList(Long userId, int year, int month); 27 | 28 | List getDailyScraps(Long userId, LocalDate date); 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/exception/dto/SuccessResponse.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.exception.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 5 | import org.terning.terningserver.common.exception.enums.SuccessMessage; 6 | import org.terning.terningserver.common.exception.SuccessCode; 7 | 8 | @JsonPropertyOrder({"status", "message", "result"}) 9 | public record SuccessResponse( 10 | int status, 11 | String message, 12 | @JsonInclude(JsonInclude.Include.NON_NULL) 13 | T result 14 | ) { 15 | public static SuccessResponse of(SuccessMessage successMessage){ 16 | return new SuccessResponse(successMessage.getStatus(), successMessage.getMessage(), null); 17 | } 18 | 19 | public static SuccessResponse of(SuccessMessage successMessage, T result){ 20 | return new SuccessResponse(successMessage.getStatus(), successMessage.getMessage(), result); 21 | } 22 | 23 | public static SuccessResponse of(SuccessCode successCode) { 24 | return new SuccessResponse<>(successCode.getStatus().value(), successCode.getMessage(), null); 25 | } 26 | 27 | public static SuccessResponse of(SuccessCode successCode, T result) { 28 | return new SuccessResponse<>(successCode.getStatus().value(), successCode.getMessage(), result); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/config/LoginUserArgumentResolver.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.config; 2 | 3 | import org.springframework.core.MethodParameter; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.web.bind.support.WebDataBinderFactory; 6 | import org.springframework.web.context.request.NativeWebRequest; 7 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 8 | import org.springframework.web.method.support.ModelAndViewContainer; 9 | 10 | @Component 11 | public class LoginUserArgumentResolver implements HandlerMethodArgumentResolver { 12 | 13 | private static final String USER_ID_ATTRIBUTE_NAME = "userId"; 14 | 15 | @Override 16 | public boolean supportsParameter(MethodParameter parameter) { 17 | boolean hasLoginAnnotation = parameter.hasParameterAnnotation(Login.class); 18 | boolean isLongType = Long.class.isAssignableFrom(parameter.getParameterType()) || parameter.getParameterType().equals(long.class); 19 | 20 | return hasLoginAnnotation && isLongType; 21 | } 22 | 23 | @Override 24 | public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, 25 | NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { 26 | return webRequest.getAttribute(USER_ID_ATTRIBUTE_NAME, NativeWebRequest.SCOPE_REQUEST); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/scrap/domain/Color.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.scrap.domain; 2 | 3 | 4 | import lombok.Getter; 5 | import lombok.RequiredArgsConstructor; 6 | import org.terning.terningserver.common.exception.CustomException; 7 | 8 | import java.util.Arrays; 9 | import java.util.Map; 10 | import java.util.stream.Collectors; 11 | 12 | import static org.terning.terningserver.common.exception.enums.ErrorMessage.INVALID_SCRAP_COLOR; 13 | 14 | @RequiredArgsConstructor 15 | @Getter 16 | public enum Color { 17 | 18 | RED("red", "ED4E54"), 19 | ORANGE("orange", "F3A649"), 20 | LIGHT_GREEN("lightgreen", "C4E953"), 21 | MINT("mint", "45D0CC"), 22 | PURPLE("purple", "9B64E2"), 23 | CORAL("coral", "EE7647"), 24 | YELLOW("yellow", "F5E660"), 25 | GREEN("green", "84D558"), 26 | BLUE("blue", "4AA9F2"), 27 | PINK("pink", "F260AC"); 28 | 29 | private final String name; 30 | private final String value; 31 | 32 | private static final Map colorMap = 33 | Arrays.stream(Color.values()) 34 | .collect(Collectors.toMap(Color::getName, color -> color)); 35 | 36 | public String getColorValue() { 37 | return "#" + value; 38 | } 39 | 40 | public static Color findByName(String name) { 41 | Color color = colorMap.get(name); 42 | if (color == null) { 43 | throw new CustomException(INVALID_SCRAP_COLOR); 44 | } 45 | return color; 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/user/domain/UserSyncEvent.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.user.domain; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.EnumType; 5 | import jakarta.persistence.Enumerated; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.GenerationType; 8 | import jakarta.persistence.Id; 9 | import jakarta.persistence.Table; 10 | import java.time.LocalDateTime; 11 | import lombok.AccessLevel; 12 | import lombok.Builder; 13 | import lombok.Getter; 14 | import lombok.NoArgsConstructor; 15 | 16 | @Entity 17 | @Getter 18 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 19 | @Table(name = "user_sync_events") 20 | public class UserSyncEvent { 21 | 22 | @Id 23 | @GeneratedValue(strategy = GenerationType.IDENTITY) 24 | private Long id; 25 | 26 | private Long userId; 27 | 28 | @Enumerated(EnumType.STRING) 29 | private UserSyncEventType eventType; 30 | 31 | private String newValue; 32 | 33 | private LocalDateTime createdAt; 34 | 35 | private boolean processed; 36 | 37 | @Builder 38 | public UserSyncEvent(Long userId, UserSyncEventType eventType, String newValue, LocalDateTime createdAt) { 39 | this.userId = userId; 40 | this.eventType = eventType; 41 | this.newValue = newValue; 42 | this.createdAt = createdAt; 43 | this.processed = false; 44 | } 45 | 46 | public void markProcessed() { 47 | this.processed = true; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/user/config/UserSyncJobConfig.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.user.config; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.batch.core.Job; 5 | import org.springframework.batch.core.Step; 6 | import org.springframework.batch.core.job.builder.JobBuilder; 7 | import org.springframework.batch.core.repository.JobRepository; 8 | import org.springframework.batch.core.step.builder.StepBuilder; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | import org.springframework.transaction.PlatformTransactionManager; 12 | 13 | @Configuration 14 | @RequiredArgsConstructor 15 | public class UserSyncJobConfig { 16 | 17 | public static final String USER_SYNC_JOB = "userSyncJob"; 18 | public static final String USER_SYNC_STEP = "userSyncStep"; 19 | 20 | private final NotificationSyncTasklet notificationSyncTasklet; 21 | 22 | @Bean 23 | public Job userSyncJob(JobRepository jobRepository, Step userSyncStep) { 24 | return new JobBuilder(USER_SYNC_JOB, jobRepository) 25 | .start(userSyncStep) 26 | .build(); 27 | } 28 | 29 | @Bean 30 | public Step userSyncStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) { 31 | return new StepBuilder(USER_SYNC_STEP, jobRepository) 32 | .tasklet(notificationSyncTasklet, transactionManager) 33 | .build(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/api/UserSwagger.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.api; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.Parameter; 5 | import io.swagger.v3.oas.annotations.tags.Tag; 6 | import org.springframework.http.ResponseEntity; 7 | import org.terning.terningserver.auth.config.Login; 8 | import org.terning.terningserver.user.dto.request.ProfileUpdateRequestDto; 9 | import org.terning.terningserver.user.dto.request.PushStatusUpdateRequest; 10 | import org.terning.terningserver.user.dto.response.ProfileResponseDto; 11 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 12 | 13 | @Tag(name = "Mypage", description = "마이페이지 관련 API") 14 | public interface UserSwagger { 15 | 16 | @Operation(summary = "마이페이지 > 프로필 정보 불러오기", description = "마이페이지에서 프로필 정보를 불러오는 API") 17 | ResponseEntity> getProfile( 18 | @Parameter(hidden = true) @Login Long userId 19 | ); 20 | 21 | @Operation(summary = "마이페이지 > 프로필 정보 수정하기", description = "마이페이지에서 프로필 정보를 수정하는 API") 22 | ResponseEntity updateProfile( 23 | @Parameter(hidden = true) @Login Long userId, 24 | ProfileUpdateRequestDto request 25 | ); 26 | 27 | @Operation(summary = "마이페이지 > 푸시알림 상태 변경하기", description = "마이페이지에서 푸시알림 허용 여부를 수정하는 API") 28 | ResponseEntity> updatePushStatus( 29 | @Parameter(hidden = true) @Login Long userId, 30 | PushStatusUpdateRequest request 31 | ); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/scrap/api/ScrapSwagger.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.scrap.api; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.tags.Tag; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.PathVariable; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.terning.terningserver.auth.config.Login; 9 | import org.terning.terningserver.scrap.dto.request.CreateScrapRequestDto; 10 | import org.terning.terningserver.scrap.dto.request.UpdateScrapRequestDto; 11 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 12 | 13 | @Tag(name="Scrap", description = "스크랩 관련 API") 14 | public interface ScrapSwagger { 15 | 16 | @Operation(summary = "스크랩 추가", description = "사용자가 스크랩을 추가하는 API") 17 | ResponseEntity createScrap( 18 | @Login long userId, 19 | @PathVariable long internshipAnnouncementId, 20 | @RequestBody CreateScrapRequestDto request 21 | ); 22 | 23 | @Operation(summary = "스크랩 취소", description = "사용자가 스크랩을 취소하는 API") 24 | ResponseEntity deleteScrap( 25 | @Login long userId, 26 | @PathVariable long internshipAnnouncementId 27 | ); 28 | 29 | @Operation(summary = "스크랩 수정", description = "사용자가 스크랩 색상을 수정하는 API") 30 | public ResponseEntity updateScrapColor( 31 | @Login long userId, 32 | @PathVariable long scrapId, 33 | @RequestBody UpdateScrapRequestDto request 34 | ); 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/exception/dto/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.exception.dto; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import org.springframework.validation.BindingResult; 6 | import org.springframework.validation.FieldError; 7 | 8 | import java.util.List; 9 | 10 | @Builder 11 | public record ErrorResponse( 12 | int status, 13 | String message 14 | ) { 15 | public static ErrorResponse of(int status, String message){ 16 | return ErrorResponse.builder() 17 | .status(status) 18 | .message(message) 19 | .build(); 20 | } 21 | 22 | public static ErrorResponse of(int status, String message, BindingResult bindingResult){ 23 | return ErrorResponse.builder() 24 | .status(status) 25 | .message(message) 26 | .build(); 27 | } 28 | 29 | /** 30 | * Q: record 에서는 getter 를 만들어 주는데 가독성을 위해서 남겨둘지 말지 31 | */ 32 | @Getter 33 | public static class ValidationError { 34 | private final String field; 35 | private final String message; 36 | 37 | private ValidationError(FieldError fieldError){ 38 | this.field = fieldError.getField(); 39 | this.message = fieldError.getDefaultMessage(); 40 | } 41 | 42 | public static List of(final BindingResult bindingResult){ 43 | return bindingResult.getFieldErrors().stream() 44 | .map(ValidationError::new) 45 | .toList(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/notification/FcmTokenValidationClient.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.notification; 2 | 3 | import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.reactive.function.client.WebClient; 8 | import org.terning.terningserver.external.pushNotification.dto.FcmTokenReissueRequiredResponse; 9 | 10 | @Slf4j 11 | @RequiredArgsConstructor 12 | @Component 13 | public class FcmTokenValidationClient { 14 | 15 | private static final String FCM_VALIDATION_PATH = "/api/v1/users/{userId}/fcm-tokens/reissue-required"; 16 | private static final String FAIL_LOG = "FCM 토큰 유효성 검사 요청 실패"; 17 | private static final String FAIL_LOG_FALLBACK = FAIL_LOG + " (fallback) - userId: {}"; 18 | 19 | private final WebClient notificationWebClient; 20 | 21 | @CircuitBreaker(name = "fcmValidation", fallbackMethod = "fallback") 22 | public boolean requestFcmTokenValidation(Long userId) { 23 | FcmTokenReissueRequiredResponse response = notificationWebClient.get() 24 | .uri(FCM_VALIDATION_PATH, userId) 25 | .retrieve() 26 | .bodyToMono(FcmTokenReissueRequiredResponse.class) 27 | .block(); 28 | 29 | return response != null && response.reissueRequired(); 30 | } 31 | 32 | public boolean fallback(Long userId, Throwable t) { 33 | log.warn(FAIL_LOG_FALLBACK, userId, t); 34 | return false; 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/search/api/SearchSwagger.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.search.api; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.Parameter; 5 | import io.swagger.v3.oas.annotations.tags.Tag; 6 | import org.springframework.data.domain.Pageable; 7 | import org.springframework.http.ResponseEntity; 8 | import org.terning.terningserver.auth.config.Login; 9 | import org.terning.terningserver.search.dto.response.PopularAnnouncementListResponseDto; 10 | import org.springframework.web.bind.annotation.RequestParam; 11 | import org.terning.terningserver.search.dto.response.SearchResultResponseDto; 12 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 13 | 14 | @Tag(name = "Search", description = "탐색 관련 API") 15 | public interface SearchSwagger { 16 | 17 | @Operation(summary = "탐색 > 지금 조회수 많은 공고", description = "탐색 화면에서 조회수 많은 공고를 불러오는 API") 18 | ResponseEntity> getMostViewedAnnouncements( 19 | 20 | ); 21 | 22 | @Operation(summary = "탐색 > 지금 스크랩 수 많은 공고", description = "탐색 화면에서 스크랩 수 많은 공고를 불러오는 API") 23 | ResponseEntity> getMostScrappedAnnouncements( 24 | 25 | ); 26 | 27 | @Operation(summary = "탐색 > 검색 결과 화면", description = "탐색 화면에서 인턴 공고를 검색하는 API") 28 | ResponseEntity> searchInternshipAnnouncement( 29 | @Parameter(hidden = true) @Login Long userId, 30 | @RequestParam(value = "keyword", required = false) String keyword, 31 | @RequestParam("sortBy") String sortBy, Pageable pageable 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/calendar/dto/response/DailyScrapResponseDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.calendar.dto.response; 2 | 3 | import lombok.Builder; 4 | import org.terning.terningserver.scrap.domain.Scrap; 5 | import org.terning.terningserver.common.util.DateUtil; 6 | 7 | @Builder 8 | public record DailyScrapResponseDto( 9 | Long internshipAnnouncementId, 10 | String companyImage, 11 | String dDay, 12 | String title, 13 | String workingPeriod, 14 | boolean isScrapped, 15 | String color, 16 | String deadline, 17 | String startYearMonth 18 | ) { 19 | public static DailyScrapResponseDto of(final Scrap scrap){ 20 | String startYearMonth = scrap.getInternshipAnnouncement().getStartYear() + "년 " + scrap.getInternshipAnnouncement().getStartMonth() + "월"; 21 | String deadline = DateUtil.convertDeadline(scrap.getInternshipAnnouncement().getDeadline()); 22 | 23 | return DailyScrapResponseDto.builder() 24 | .internshipAnnouncementId(scrap.getInternshipAnnouncement().getId()) 25 | .companyImage(scrap.getInternshipAnnouncement().getCompany().getCompanyImage()) 26 | .dDay(DateUtil.convert(scrap.getInternshipAnnouncement().getDeadline())) 27 | .title(scrap.getInternshipAnnouncement().getTitle()) 28 | .workingPeriod(scrap.getInternshipAnnouncement().getWorkingPeriod()) 29 | .isScrapped(true) // 스크랩된 경우에만 DTO 생성하므로 true 30 | .color(scrap.getColorToHexValue()) 31 | .deadline(deadline) 32 | .startYearMonth(startYearMonth) 33 | .build(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/home/dto/response/HomeResponseDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.home.dto.response; 2 | 3 | import lombok.Builder; 4 | import org.terning.terningserver.internshipAnnouncement.domain.InternshipAnnouncement; 5 | import org.terning.terningserver.common.util.DateUtil; 6 | 7 | @Builder 8 | public record HomeResponseDto( 9 | Long internshipAnnouncementId, 10 | String companyImage, 11 | String dDay, 12 | String title, 13 | String workingPeriod, 14 | boolean isScrapped, 15 | String color, 16 | String deadline, 17 | String startYearMonth 18 | ) { 19 | public static HomeResponseDto of(final InternshipAnnouncement internshipAnnouncement, final boolean isScrapped, final String color){ 20 | String dDay = DateUtil.convert(internshipAnnouncement.getDeadline()); // dDay 계산 로직 추가 21 | String startYearMonth = internshipAnnouncement.getStartYear() + "년 " + internshipAnnouncement.getStartMonth() + "월"; 22 | String deadline = DateUtil.convertDeadline(internshipAnnouncement.getDeadline()); 23 | 24 | return HomeResponseDto.builder() 25 | .internshipAnnouncementId(internshipAnnouncement.getId()) 26 | .companyImage(internshipAnnouncement.getCompany().getCompanyImage()) 27 | .dDay(dDay) 28 | .title(internshipAnnouncement.getTitle()) 29 | .workingPeriod(internshipAnnouncement.getWorkingPeriod()) 30 | .isScrapped(isScrapped) 31 | .color(color) 32 | .deadline(deadline) 33 | .startYearMonth(startYearMonth) 34 | .build(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/internshipAnnouncement/api/InternshipDetailController.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.internshipAnnouncement.api; 2 | 3 | 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import org.terning.terningserver.auth.config.Login; 11 | import org.terning.terningserver.internshipAnnouncement.dto.response.InternshipDetailResponseDto; 12 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 13 | import org.terning.terningserver.internshipAnnouncement.application.InternshipDetailService; 14 | 15 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_GET_INTERNSHIP_DETAIL; 16 | 17 | @RestController 18 | @RequiredArgsConstructor 19 | @RequestMapping("/api/v1") 20 | public class InternshipDetailController implements InternshipDetailSwagger { 21 | 22 | private final InternshipDetailService internshipDetailService; 23 | 24 | @GetMapping("/announcements/{internshipAnnouncementId}") 25 | public ResponseEntity> getInternshipDetail( 26 | @Login Long userId, 27 | @PathVariable Long internshipAnnouncementId) { 28 | return ResponseEntity.ok(SuccessResponse.of( 29 | SUCCESS_GET_INTERNSHIP_DETAIL, 30 | internshipDetailService.getInternshipDetail(internshipAnnouncementId, userId) 31 | )); 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/scrap/config/OpsApiClient.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.scrap.config; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Component; 5 | import org.springframework.web.reactive.function.client.WebClient; 6 | import org.terning.terningserver.external.pushNotification.scrap.application.port.ScrapSyncNotifier; 7 | import org.terning.terningserver.external.pushNotification.scrap.common.failure.ScrapExternalErrorCode; 8 | import org.terning.terningserver.external.pushNotification.scrap.common.failure.ScrapExternalException; 9 | import org.terning.terningserver.external.pushNotification.scrap.dto.request.ScrapUserIdsRequest; 10 | 11 | import java.util.List; 12 | 13 | @Component 14 | @RequiredArgsConstructor 15 | public class OpsApiClient implements ScrapSyncNotifier { 16 | 17 | private final WebClient operationBaseUrlWebClient; 18 | 19 | @Override 20 | public void notify(List userIds) { 21 | if (userIds == null || userIds.isEmpty()) { 22 | throw new ScrapExternalException(ScrapExternalErrorCode.EMPTY_USER_IDS); 23 | } 24 | 25 | ScrapUserIdsRequest request = ScrapUserIdsRequest.of(userIds); 26 | try { 27 | operationBaseUrlWebClient.post() 28 | .uri("/api/v1/external/scraps/sync/result") 29 | .bodyValue(request) 30 | .retrieve() 31 | .toBodilessEntity() 32 | .block(); 33 | } catch (Exception e) { 34 | throw new ScrapExternalException(ScrapExternalErrorCode.FAILED_TO_SEND_NOTIFICATION); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/calendar/api/CalendarSwagger.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.calendar.api; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.Parameter; 5 | import io.swagger.v3.oas.annotations.tags.Tag; 6 | import org.springframework.http.ResponseEntity; 7 | import org.terning.terningserver.auth.config.Login; 8 | import org.terning.terningserver.calendar.dto.response.DailyScrapResponseDto; 9 | import org.terning.terningserver.calendar.dto.response.MonthlyDefaultResponseDto; 10 | import org.terning.terningserver.calendar.dto.response.MonthlyListResponseDto; 11 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 12 | 13 | import java.util.List; 14 | 15 | @Tag(name = "Calendar", description = "캘린더 관련 API") 16 | public interface CalendarSwagger { 17 | 18 | @Operation(summary = "캘린더 > 월간 스크랩 공고 조회", description = "월간 스크랩 공고를 조회하는 API") 19 | ResponseEntity>> getMonthlyScraps( 20 | @Parameter(hidden = true) @Login Long userId, 21 | int year, 22 | int month 23 | ); 24 | 25 | @Operation(summary = "캘린더 > 월간 스크랩 공고 조회 (리스트)", description = "월간 스크랩 공고를 리스트로 조회하는 API") 26 | ResponseEntity>> getMonthlyScrapsAsList( 27 | @Parameter(hidden = true) @Login Long userId, 28 | int year, 29 | int month 30 | ); 31 | 32 | @Operation(summary = "캘린더 > 일간 스크랩 공고 조회 (리스트)", description = "일간 스크랩 공고를 리스트로 조회하는 API") 33 | ResponseEntity>> getDailyScraps( 34 | @Parameter(hidden = true) @Login Long userId, 35 | String date 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/application/social/kakao/KakaoAuthTokenValidator.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.application.social.kakao; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.google.gson.JsonArray; 5 | import lombok.RequiredArgsConstructor; 6 | import lombok.val; 7 | import org.springframework.http.HttpEntity; 8 | import org.springframework.http.HttpHeaders; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.transaction.annotation.Transactional; 11 | import org.springframework.web.client.RestTemplate; 12 | import org.terning.terningserver.common.config.ValueConfig; 13 | import org.terning.terningserver.common.exception.CustomException; 14 | 15 | import java.util.Map; 16 | 17 | import static org.terning.terningserver.common.exception.enums.ErrorMessage.FAILED_SOCIAL_LOGIN; 18 | 19 | @Component 20 | @RequiredArgsConstructor 21 | @Transactional(readOnly = true) 22 | public class KakaoAuthTokenValidator { 23 | 24 | private final ValueConfig valueConfig; 25 | private final RestTemplate restTemplate; 26 | private final ObjectMapper objectMapper; 27 | 28 | public String extractKakaoId(String authAccessToken) { 29 | try { 30 | val headers = new HttpHeaders(); 31 | headers.add("Authorization", authAccessToken); 32 | val httpEntity = new HttpEntity(headers); 33 | val responseData = restTemplate.postForEntity(valueConfig.getKakaoUri(), httpEntity, Object.class); 34 | return objectMapper.convertValue(responseData.getBody(), Map.class).get("id").toString(); 35 | } catch (Exception exception) { 36 | throw new CustomException(FAILED_SOCIAL_LOGIN); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/config/LoginCheckInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.config; 2 | 3 | import jakarta.servlet.http.HttpServletRequest; 4 | import jakarta.servlet.http.HttpServletResponse; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.stereotype.Component; 7 | import org.springframework.web.servlet.HandlerInterceptor; 8 | import org.terning.terningserver.auth.jwt.JwtProvider; 9 | import org.terning.terningserver.auth.jwt.exception.JwtException; 10 | 11 | import java.util.Optional; 12 | 13 | @Component 14 | @RequiredArgsConstructor 15 | public class LoginCheckInterceptor implements HandlerInterceptor { 16 | 17 | private final JwtProvider jwtProvider; 18 | 19 | private static final String AUTHORIZATION_HEADER = "Authorization"; 20 | private static final String USER_ID_ATTRIBUTE_NAME = "userId"; 21 | 22 | @Override 23 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 24 | 25 | if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { 26 | return true; 27 | } 28 | 29 | try { 30 | Optional userIdOpt = Optional.ofNullable(request.getHeader(AUTHORIZATION_HEADER)) 31 | .map(jwtProvider::getUserIdFrom); 32 | 33 | if (userIdOpt.isEmpty()) { 34 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 35 | return false; 36 | } 37 | 38 | request.setAttribute(USER_ID_ATTRIBUTE_NAME, userIdOpt.get()); 39 | return true; 40 | 41 | } catch (JwtException e) { 42 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 43 | return false; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/domain/PushNotificationStatus.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.domain; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonValue; 5 | import org.terning.terningserver.common.exception.CustomException; 6 | import org.terning.terningserver.common.exception.enums.ErrorMessage; 7 | 8 | import java.util.Arrays; 9 | 10 | public enum PushNotificationStatus { 11 | ENABLED("enabled"), 12 | DISABLED("disabled"); 13 | 14 | private final String value; 15 | 16 | PushNotificationStatus(String value) { 17 | this.value = value; 18 | } 19 | 20 | @JsonCreator 21 | public static PushNotificationStatus from(String input) { 22 | return Arrays.stream(values()) 23 | .filter(status -> status.value.equalsIgnoreCase(input)) 24 | .findFirst() 25 | .orElseThrow(() -> new CustomException(ErrorMessage.INVALID_FORMAT_ERROR)); 26 | } 27 | 28 | public static boolean isValid(String input) { 29 | return Arrays.stream(values()) 30 | .anyMatch(status -> status.value.equalsIgnoreCase(input)); 31 | } 32 | 33 | public boolean canReceiveNotification() { 34 | return this == ENABLED; 35 | } 36 | 37 | public PushNotificationStatus enable() { 38 | if (this == ENABLED) { 39 | throw new CustomException(ErrorMessage.ALREADY_ENABLED_PUSH_NOTIFICATION); 40 | } 41 | return ENABLED; 42 | } 43 | 44 | public PushNotificationStatus disable() { 45 | if (this == DISABLED) { 46 | throw new CustomException(ErrorMessage.ALREADY_DISABLED_PUSH_NOTIFICATION); 47 | } 48 | return DISABLED; 49 | } 50 | 51 | @JsonValue 52 | public String value() { 53 | return value; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/calendar/dto/response/MonthlyListResponseDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.calendar.dto.response; 2 | 3 | import lombok.Builder; 4 | 5 | import java.util.List; 6 | 7 | @Builder 8 | public record MonthlyListResponseDto( 9 | String deadline, 10 | List announcements 11 | ) { 12 | @Builder 13 | public static record ScrapDetail( 14 | Long internshipAnnouncementId, 15 | String companyImage, 16 | String dDay, 17 | String title, 18 | String workingPeriod, 19 | boolean isScrapped, 20 | String color, 21 | String deadline, 22 | String startYearMonth 23 | ){ 24 | public static ScrapDetail of(final Long internshipAnnouncementId, final String companyImage, final String dDay, 25 | final String title, final String workingPeriod, final boolean isScrapped, 26 | final String color, final String deadline, final String startYearMonth){ 27 | return ScrapDetail.builder() 28 | .internshipAnnouncementId(internshipAnnouncementId) 29 | .companyImage(companyImage) 30 | .dDay(dDay) 31 | .title(title) 32 | .workingPeriod(workingPeriod) 33 | .isScrapped(isScrapped) 34 | .color(color) 35 | .deadline(deadline) 36 | .startYearMonth(startYearMonth) 37 | .build(); 38 | } 39 | } 40 | 41 | public static MonthlyListResponseDto of(String deadline, List announcements){ 42 | return MonthlyListResponseDto.builder() 43 | .deadline(deadline) 44 | .announcements(announcements) 45 | .build(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/logging/CachedHttpServletRequest.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.logging; 2 | 3 | import jakarta.servlet.ReadListener; 4 | import jakarta.servlet.ServletInputStream; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletRequestWrapper; 7 | import org.springframework.util.StreamUtils; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.ByteArrayInputStream; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | 14 | public class CachedHttpServletRequest extends HttpServletRequestWrapper { 15 | 16 | private byte[] cachedBody; 17 | 18 | public CachedHttpServletRequest(HttpServletRequest request) throws IOException { 19 | super(request); 20 | this.cachedBody = StreamUtils.copyToByteArray(request.getInputStream()); 21 | } 22 | 23 | @Override 24 | public BufferedReader getReader() { 25 | return new BufferedReader(new InputStreamReader(new ByteArrayInputStream(this.cachedBody))); 26 | } 27 | 28 | @Override 29 | public ServletInputStream getInputStream() { 30 | final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(cachedBody); 31 | 32 | return new ServletInputStream() { 33 | @Override 34 | public boolean isFinished() { 35 | return byteArrayInputStream.available() == 0; 36 | } 37 | 38 | @Override 39 | public boolean isReady() { 40 | return true; 41 | } 42 | 43 | @Override 44 | public void setReadListener(ReadListener readListener) { 45 | throw new UnsupportedOperationException(); 46 | } 47 | 48 | @Override 49 | public int read() throws IOException { 50 | return byteArrayInputStream.read(); 51 | } 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/filter/domain/Filter.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.filter.domain; 2 | 3 | import jakarta.persistence.Entity; 4 | import jakarta.persistence.EnumType; 5 | import jakarta.persistence.Enumerated; 6 | import jakarta.persistence.GeneratedValue; 7 | import jakarta.persistence.Id; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Builder; 10 | import lombok.Getter; 11 | import lombok.NoArgsConstructor; 12 | import org.terning.terningserver.auth.dto.request.SignUpFilterRequest; 13 | 14 | import static lombok.AccessLevel.PROTECTED; 15 | import static jakarta.persistence.GenerationType.IDENTITY; 16 | 17 | @Entity 18 | @Getter 19 | @NoArgsConstructor(access = PROTECTED) 20 | @AllArgsConstructor 21 | @Builder 22 | public class Filter { 23 | 24 | @Id 25 | @GeneratedValue(strategy = IDENTITY) 26 | private Long id; 27 | 28 | @Enumerated(EnumType.STRING) 29 | private JobType jobType; 30 | 31 | @Enumerated(EnumType.STRING) 32 | private Grade grade; 33 | 34 | @Enumerated(EnumType.STRING) 35 | private WorkingPeriod workingPeriod; 36 | 37 | private int startYear; 38 | 39 | private int startMonth; 40 | 41 | public static Filter from(SignUpFilterRequest request) { 42 | return Filter.builder() 43 | .jobType(JobType.TOTAL) 44 | .grade(Grade.fromKey(request.grade())) 45 | .workingPeriod(WorkingPeriod.fromKey(request.workingPeriod())) 46 | .startYear(request.startYear()) 47 | .startMonth(request.startMonth()) 48 | .build(); 49 | } 50 | 51 | public void updateFilter(JobType jobType, Grade grade, WorkingPeriod workingPeriod, int startYear, int startMonth) { 52 | this.jobType = jobType; 53 | this.grade = grade; 54 | this.workingPeriod = workingPeriod; 55 | this.startYear = startYear; 56 | this.startMonth = startMonth; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/home/dto/response/UpcomingScrapResponseDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.home.dto.response; 2 | 3 | import lombok.Builder; 4 | import org.terning.terningserver.scrap.domain.Scrap; 5 | import org.terning.terningserver.common.util.DateUtil; 6 | 7 | import java.util.List; 8 | 9 | @Builder 10 | public record UpcomingScrapResponseDto( 11 | boolean hasScrapped, 12 | List scraps 13 | ) { 14 | @Builder 15 | public record ScrapDetail( 16 | Long internshipAnnouncementId, 17 | String companyImage, 18 | String dDay, 19 | String title, 20 | String workingPeriod, 21 | boolean isScrapped, 22 | String color, 23 | String deadline, 24 | String startYearMonth, 25 | String companyInfo 26 | ) { 27 | public static ScrapDetail of(final Scrap scrap) { 28 | String startYearMonth = scrap.getInternshipAnnouncement().getStartYear() + "년 " + scrap.getInternshipAnnouncement().getStartMonth() + "월"; 29 | 30 | return ScrapDetail.builder() 31 | .internshipAnnouncementId(scrap.getInternshipAnnouncement().getId()) 32 | .companyImage(scrap.getInternshipAnnouncement().getCompany().getCompanyImage()) 33 | .dDay(DateUtil.convert(scrap.getInternshipAnnouncement().getDeadline())) 34 | .title(scrap.getInternshipAnnouncement().getTitle()) 35 | .deadline(DateUtil.convertDeadline(scrap.getInternshipAnnouncement().getDeadline())) 36 | .isScrapped(true) // 스크랩된 항목이므로 항상 true 37 | .color(scrap.getColorToHexValue()) 38 | .workingPeriod(scrap.getInternshipAnnouncement().getWorkingPeriod()) 39 | .startYearMonth(startYearMonth) 40 | .companyInfo(scrap.getInternshipAnnouncement().getCompany().getCompanyInfo()) 41 | .build(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/resources/logback-dev.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | System.out 9 | 10 | %d{yyyy-MM-dd HH:mm:ss,Asia/Seoul} [%thread] %-5level %logger{36} - %msg%n 11 | 12 | 13 | 14 | 15 | 16 | System.err 17 | 18 | %d{yyyy-MM-dd HH:mm:ss,Asia/Seoul} [%thread] %-5level %logger{36} - %msg%n 19 | 20 | 21 | 22 | 23 | 24 | ${LOG_FILE} 25 | 26 | ${LOG_PATH}/terning-%d{yyyy-MM-dd}.log 27 | 14 28 | 29 | 30 | %d{yyyy-MM-dd HH:mm:ss,Asia/Seoul} [%thread] %-5level %logger{36} [%X{traceId:-NoTraceID}] - %msg%n 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/resources/logback-prod.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | System.out 9 | 10 | %d{yyyy-MM-dd HH:mm:ss,Asia/Seoul} [%thread] %-5level %logger{36} - %msg%n 11 | 12 | 13 | 14 | 15 | 16 | System.err 17 | 18 | %d{yyyy-MM-dd HH:mm:ss,Asia/Seoul} [%thread] %-5level %logger{36} - %msg%n 19 | 20 | 21 | 22 | 23 | 24 | ${LOG_FILE} 25 | 26 | ${LOG_PATH}/terning-%d{yyyy-MM-dd}.log 27 | 14 28 | 29 | 30 | %d{yyyy-MM-dd HH:mm:ss,Asia/Seoul} [%thread] %-5level %logger{36} [%X{traceId:-NoTraceID}] - %msg%n 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/filter/api/FilterController.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.filter.api; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PutMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import org.terning.terningserver.auth.config.Login; 11 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 12 | import org.terning.terningserver.filter.application.FilterService; 13 | import org.terning.terningserver.filter.dto.request.UpdateUserFilterRequestDto; 14 | import org.terning.terningserver.filter.dto.response.UserFilterDetailResponseDto; 15 | 16 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_GET_USER_FILTER; 17 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_UPDATE_USER_FILTER; 18 | 19 | @RestController 20 | @RequiredArgsConstructor 21 | @RequestMapping("/api/v1") 22 | public class FilterController implements FilterSwagger { 23 | 24 | private final FilterService filterService; 25 | 26 | @GetMapping("/filters") 27 | public ResponseEntity> getUserFilter( 28 | @Login long userId 29 | ) { 30 | return ResponseEntity.ok(SuccessResponse.of( 31 | SUCCESS_GET_USER_FILTER, 32 | filterService.getUserFilter(userId) 33 | )); 34 | } 35 | 36 | @PutMapping("/filters") 37 | public ResponseEntity updateUserFilter( 38 | @Login long userId, 39 | @RequestBody UpdateUserFilterRequestDto requestDto 40 | ) { 41 | filterService.updateUserFilter(requestDto, userId); 42 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_UPDATE_USER_FILTER)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/user/scheduler/UserSyncScheduler.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.user.scheduler; 2 | 3 | import java.time.LocalDateTime; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.batch.core.Job; 7 | import org.springframework.batch.core.JobParameters; 8 | import org.springframework.batch.core.JobParametersBuilder; 9 | import org.springframework.batch.core.launch.JobLauncher; 10 | import org.springframework.scheduling.annotation.Scheduled; 11 | import org.springframework.stereotype.Component; 12 | 13 | @Component 14 | @RequiredArgsConstructor 15 | @Slf4j 16 | public class UserSyncScheduler { 17 | 18 | private static final String JOB_PARAM_TIMESTAMP = "timestamp"; 19 | 20 | private final JobLauncher jobLauncher; 21 | private final Job userSyncJob; 22 | 23 | /** 24 | * 매주 목, 토 오후 12:30에 실행 (푸시 알림 전송 30분 전) 25 | */ 26 | @Scheduled(cron = "0 30 12 ? * THU,SAT", zone = "Asia/Seoul") 27 | public void runUserSyncJobBeforeRecommendation() throws Exception { 28 | JobParameters jobParameters = new JobParametersBuilder() 29 | .addLong(JOB_PARAM_TIMESTAMP, System.currentTimeMillis()) 30 | .toJobParameters(); 31 | jobLauncher.run(userSyncJob, jobParameters); 32 | log.info("User sync job for push status/name update (before recommendation) triggered at {}", LocalDateTime.now()); 33 | } 34 | 35 | /** 36 | * 매주 일요일 오후 8:30에 실행 (푸시 알림 전송 30분 전) 37 | */ 38 | @Scheduled(cron = "0 30 20 ? * SUN", zone = "Asia/Seoul") 39 | public void runUserSyncJobBeforeTrendingAlert() throws Exception { 40 | JobParameters jobParameters = new JobParametersBuilder() 41 | .addLong(JOB_PARAM_TIMESTAMP, System.currentTimeMillis()) 42 | .toJobParameters(); 43 | jobLauncher.run(userSyncJob, jobParameters); 44 | log.info("User sync job for push status/name update (before trending alert) triggered at {}", LocalDateTime.now()); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/config/WebClientConfig.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.config; 2 | 3 | import io.netty.channel.ChannelOption; 4 | import io.netty.handler.timeout.ReadTimeoutHandler; 5 | import io.netty.handler.timeout.WriteTimeoutHandler; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.http.HttpHeaders; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.web.reactive.function.client.WebClient; 12 | import reactor.netty.http.client.HttpClient; 13 | import reactor.netty.tcp.TcpClient; 14 | import org.springframework.http.client.reactive.ReactorClientHttpConnector; 15 | 16 | import java.util.concurrent.TimeUnit; 17 | 18 | @Configuration 19 | public class WebClientConfig { 20 | 21 | private static final int CONNECT_TIMEOUT_MILLIS = 5000; 22 | private static final int READ_TIMEOUT_SECONDS = 2; 23 | private static final int WRITE_TIMEOUT_SECONDS = 2; 24 | 25 | private static final String DEFAULT_CONTENT_TYPE = MediaType.APPLICATION_JSON_VALUE; 26 | 27 | @Value("${operation.base-url}") 28 | private String operationBaseUrl; 29 | 30 | @Bean 31 | public WebClient operationBaseUrlWebClient() { 32 | TcpClient tcpClient = TcpClient.create() 33 | .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT_MILLIS) 34 | .doOnConnected(conn -> conn 35 | .addHandlerLast(new ReadTimeoutHandler(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS)) 36 | .addHandlerLast(new WriteTimeoutHandler(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)) 37 | ); 38 | 39 | HttpClient httpClient = HttpClient.from(tcpClient); 40 | 41 | return WebClient.builder() 42 | .baseUrl(operationBaseUrl) 43 | .defaultHeader(HttpHeaders.CONTENT_TYPE, DEFAULT_CONTENT_TYPE) 44 | .clientConnector(new ReactorClientHttpConnector(httpClient)) 45 | .build(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/user/application/UserSyncEvnetServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.user.application; 2 | 3 | import java.time.LocalDateTime; 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.transaction.annotation.Transactional; 7 | import org.terning.terningserver.external.pushNotification.user.domain.UserSyncEvent; 8 | import org.terning.terningserver.external.pushNotification.user.domain.UserSyncEventType; 9 | import org.terning.terningserver.external.pushNotification.user.respository.UserSyncEventRepository; 10 | 11 | @Service 12 | @RequiredArgsConstructor 13 | @Transactional(readOnly = true) 14 | public class UserSyncEvnetServiceImpl implements UserSyncEventService { 15 | 16 | private final UserSyncEventRepository eventRepository; 17 | 18 | @Transactional 19 | public void recordPushStatusChange(Long userId, String newPushStatus) { 20 | UserSyncEvent event = UserSyncEvent.builder() 21 | .userId(userId) 22 | .eventType(UserSyncEventType.PUSH_STATUS_CHANGE) 23 | .newValue(newPushStatus) 24 | .createdAt(LocalDateTime.now()) 25 | .build(); 26 | eventRepository.save(event); 27 | } 28 | 29 | @Transactional 30 | public void recordNameChange(Long userId, String newName) { 31 | UserSyncEvent event = UserSyncEvent.builder() 32 | .userId(userId) 33 | .eventType(UserSyncEventType.NAME_CHANGE) 34 | .newValue(newName) 35 | .createdAt(LocalDateTime.now()) 36 | .build(); 37 | eventRepository.save(event); 38 | } 39 | 40 | @Transactional 41 | public void recordWithdraw(Long userId) { 42 | UserSyncEvent event = UserSyncEvent.builder() 43 | .userId(userId) 44 | .eventType(UserSyncEventType.WITHDRAW) 45 | .newValue(null) 46 | .createdAt(LocalDateTime.now()) 47 | .build(); 48 | eventRepository.save(event); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/scrap/domain/Scrap.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.scrap.domain; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import org.terning.terningserver.common.BaseTimeEntity; 7 | import org.terning.terningserver.user.domain.User; 8 | import org.terning.terningserver.internshipAnnouncement.domain.InternshipAnnouncement; 9 | 10 | import static jakarta.persistence.EnumType.STRING; 11 | import static lombok.AccessLevel.PROTECTED; 12 | import static jakarta.persistence.FetchType.LAZY; 13 | import static jakarta.persistence.GenerationType.IDENTITY; 14 | 15 | @Entity 16 | @Getter 17 | @NoArgsConstructor(access = PROTECTED) 18 | public class Scrap extends BaseTimeEntity { 19 | 20 | @Id 21 | @GeneratedValue(strategy = IDENTITY) 22 | private Long id; 23 | 24 | @ManyToOne(fetch = LAZY) 25 | @JoinColumn(name = "user_id", nullable = false) 26 | private User user; 27 | 28 | @ManyToOne(fetch = LAZY) 29 | @JoinColumn(name = "internship_announcement_id", nullable = false) 30 | private InternshipAnnouncement internshipAnnouncement; 31 | 32 | @Enumerated(STRING) 33 | @Column(nullable = false) 34 | private Color color; 35 | 36 | @Embedded 37 | @AttributeOverride(name = "value", column = @Column(name = "synced")) 38 | private SyncStatus syncStatus; 39 | 40 | private Scrap(User user, InternshipAnnouncement internshipAnnouncement, Color color) { 41 | this.user = user; 42 | this.internshipAnnouncement = internshipAnnouncement; 43 | this.color = color; 44 | this.syncStatus = SyncStatus.notSynced(); 45 | } 46 | 47 | public static Scrap create(User user, InternshipAnnouncement internshipAnnouncement, Color color) { 48 | return new Scrap(user, internshipAnnouncement, color); 49 | } 50 | 51 | public void updateColor(Color color) { 52 | this.color = color; 53 | } 54 | 55 | public String getColorToHexValue() { 56 | return this.color.getColorValue(); 57 | } 58 | 59 | public void markSynced() { 60 | this.syncStatus = SyncStatus.synced(); 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/internshipAnnouncement/application/InternshipDetailServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.internshipAnnouncement.application; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.transaction.annotation.Transactional; 6 | import org.terning.terningserver.internshipAnnouncement.domain.InternshipAnnouncement; 7 | import org.terning.terningserver.scrap.domain.Scrap; 8 | import org.terning.terningserver.internshipAnnouncement.dto.response.InternshipDetailResponseDto; 9 | import org.terning.terningserver.common.exception.CustomException; 10 | import org.terning.terningserver.common.exception.enums.ErrorMessage; 11 | import org.terning.terningserver.internshipAnnouncement.repository.InternshipRepository; 12 | import org.terning.terningserver.scrap.repository.ScrapRepository; 13 | 14 | import java.util.Optional; 15 | 16 | 17 | @Service 18 | @RequiredArgsConstructor 19 | public class InternshipDetailServiceImpl implements InternshipDetailService { 20 | private final InternshipRepository internshipRepository; 21 | private final ScrapRepository scrapRepository; 22 | 23 | @Override 24 | @Transactional 25 | public InternshipDetailResponseDto getInternshipDetail(long internshipAnnouncementId, long userId) { 26 | InternshipAnnouncement announcement = internshipRepository.findById(internshipAnnouncementId) 27 | .orElseThrow(() -> new CustomException(ErrorMessage.NOT_FOUND_INTERN_EXCEPTION)); 28 | 29 | announcement.updateViewCount(); 30 | Optional scrap = scrapRepository.findByInternshipAnnouncementIdAndUserId(announcement.getId(), userId); 31 | 32 | if (scrap.isPresent()) { 33 | return InternshipDetailResponseDto.of( 34 | announcement, announcement.getCompany(), 35 | scrap.get().getId(), scrap.get().getColor().getColorValue() 36 | ); 37 | } else { 38 | return InternshipDetailResponseDto.of( 39 | announcement, announcement.getCompany(), 40 | null, null 41 | ); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/exception/enums/ErrorMessage.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.exception.enums; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import org.springframework.http.HttpStatus; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | public enum ErrorMessage { 10 | 11 | // 소셜 로그인 12 | INVALID_TOKEN(401, "유효하지 않은 토큰입니다"), 13 | INVALID_KEY(401, "유효하지 않은 키입니다"), 14 | FAILED_SOCIAL_LOGIN(404, "소셜 로그인에 실패하였습니다"), 15 | INVALID_USER(404, "유효하지 않은 유저입니다"), 16 | FAILED_TOKEN_REISSUE(404, "토큰 재발급에 실패하였습니다"), 17 | UNAUTHORIZED_JWT_EXCEPTION(401, "유효하지 않은 토큰입니다"), 18 | 19 | // 필터링 20 | FAILED_SIGN_UP_USER_FILTER_CREATION(404, "사용자 필터 생성에 실패하였습니다"), 21 | FAILED_SIGN_UP_USER_FILTER_ASSIGNMENT(404, "사용자 필터 연결에 실패하였습니다"), 22 | INVALID_JOB_TYPE(401, "유효하지 않은 직무 카테고리입니다."), 23 | 24 | // 스크랩 25 | EXISTS_SCRAP_ALREADY(400, "이미 스크랩했습니다."), 26 | INVALID_SCRAP_COLOR(401, "유효하지 않은 스크랩 색상입니다."), 27 | 28 | // 로그 아웃 29 | FAILED_SIGN_OUT(404, "로그아웃에 실패하였습니다"), 30 | FAILED_REFRESH_TOKEN_RESET(400, "리프레쉬 토큰 초기화에 실패하였습니다"), 31 | 32 | // 계정 탈퇴 33 | FAILED_WITHDRAW(400, "계정 탈퇴에 실패하였습니다"), 34 | 35 | // 마이페이지 36 | INVALID_PROFILE_IMAGE(404, "유효하지 않은 프로필 이미지 입니다."), 37 | 38 | // 404(NotFound) 39 | NOT_FOUND_INTERN_CATEGORY(404, "해당 인턴 공고는 존재하지 않습니다"), 40 | NOT_FOUND_INTERN_EXCEPTION(404, "해당 인턴 공고는 존재하지 않습니다"), 41 | NOT_FOUND_USER_EXCEPTION(404, "해당 유저가 존재하지 않습니다"), 42 | NOT_FOUND_SCRAP(404, "스크랩 정보가 존재하지 않습니다"), 43 | FORBIDDEN_DELETE_SCRAP(403, "해당 유저가 스크랩하지 않았으므로 스크랩 취소가 불가합니다"), 44 | 45 | 46 | INVALID_ARGUMENT_ERROR( 400, "올바르지 않은 파라미터입니다."), 47 | INVALID_FORMAT_ERROR(400, "올바르지 않은 포맷입니다."), 48 | INVALID_TYPE_ERROR(400, "올바르지 않은 타입입니다."), 49 | ILLEGAL_ARGUMENT_ERROR( 400, "필수 파라미터가 없습니다"), 50 | INVALID_HTTP_METHOD( 400, "잘못된 Http Method 요청입니다."), 51 | INTERNAL_SERVER_ERROR( 500, "서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요."), 52 | ALREADY_ENABLED_PUSH_NOTIFICATION(400, "이미 푸시 알림이 활성화되어 있습니다."), 53 | ALREADY_DISABLED_PUSH_NOTIFICATION(400, "이미 푸시 알림이 비활성화되어 있습니다."), 54 | ; 55 | 56 | 57 | private final int status; 58 | private final String message; 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/config/SwaggerConfig.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.config; 2 | 3 | import io.swagger.v3.oas.annotations.OpenAPIDefinition; 4 | import io.swagger.v3.oas.annotations.servers.Server; 5 | import io.swagger.v3.oas.models.Components; 6 | import io.swagger.v3.oas.models.OpenAPI; 7 | import io.swagger.v3.oas.models.info.Info; 8 | import io.swagger.v3.oas.models.security.SecurityRequirement; 9 | import io.swagger.v3.oas.models.security.SecurityScheme; 10 | import org.springframework.context.annotation.Bean; 11 | import org.springframework.context.annotation.Configuration; 12 | 13 | @Configuration 14 | @OpenAPIDefinition( 15 | servers = { 16 | @Server(url = "https://www.terning-official.p-e.kr", description = "Default Server url"), 17 | @Server(url = "https://www.terning-official.n-e.kr", description = "Default Server url (2025 ver.)"), 18 | @Server(url = "http://15.165.242.132", description = "Staging Server URL"), 19 | @Server(url = "http://54.180.215.35", description = "Staging Server URL (2025 ver.)"), 20 | @Server(url = "http://localhost:8080", description = "Local Development Server URL") 21 | } 22 | ) 23 | public class SwaggerConfig { 24 | @Bean 25 | public OpenAPI openAPI() { 26 | String jwt = "JWT"; 27 | SecurityRequirement securityRequirement = new SecurityRequirement().addList(jwt); 28 | Components components = new Components().addSecuritySchemes(jwt, new SecurityScheme() 29 | .name(jwt) 30 | .type(SecurityScheme.Type.HTTP) 31 | .scheme("bearer") 32 | .bearerFormat("JWT") 33 | ); 34 | return new OpenAPI() 35 | .components(new Components()) 36 | .info(apiInfo()) 37 | .addSecurityItem(securityRequirement) 38 | .components(components); 39 | } 40 | 41 | private Info apiInfo() { 42 | return new Info() 43 | .title("Terning Point Swagger") // API의 제목 44 | .description("Terning Point Swagger ver. API 명세서") // API에 대한 설명 45 | .version("1.1.0"); // API의 버전 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/internshipAnnouncement/domain/InternshipAnnouncement.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.internshipAnnouncement.domain; 2 | 3 | import jakarta.persistence.*; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | import org.terning.terningserver.common.BaseTimeEntity; 7 | 8 | import java.time.LocalDate; 9 | import java.util.List; 10 | import org.terning.terningserver.scrap.domain.Scrap; 11 | 12 | import static jakarta.persistence.GenerationType.IDENTITY; 13 | import static lombok.AccessLevel.PROTECTED; 14 | 15 | @Entity 16 | @Getter 17 | @NoArgsConstructor(access = PROTECTED) 18 | public class InternshipAnnouncement extends BaseTimeEntity { 19 | 20 | @Id 21 | @GeneratedValue(strategy = IDENTITY) 22 | private Long id; // 고유 ID 23 | 24 | @Column(nullable = false, length = 64) 25 | private String title; // 인턴십 제목 26 | 27 | @Column(nullable = false) 28 | private LocalDate deadline; // 지원 마감일 29 | 30 | @Column(length = 16) 31 | private String workingPeriod; // 근무 기간 32 | 33 | @Column(nullable = false) 34 | private int startYear; // 시작 연도 35 | 36 | @Column(nullable = false) 37 | private int startMonth; // 시작 월 38 | 39 | @Column(nullable = false) 40 | private int viewCount; // 조회 수 41 | 42 | @Column(nullable = false) 43 | private int scrapCount; // 스크랩 수 44 | 45 | @Column(nullable = false, length = 256) 46 | private String url; // 인턴십 공고 URL 47 | 48 | @OneToMany(mappedBy = "internshipAnnouncement", cascade = CascadeType.ALL) 49 | private List scraps; 50 | 51 | @Embedded 52 | private Company company; 53 | 54 | @Column(columnDefinition = "TEXT") 55 | private String qualifications; // 자격 요건 56 | 57 | @Column(columnDefinition = "TEXT") 58 | private String jobType; // 직무 유형 59 | 60 | @Column(columnDefinition = "TEXT") 61 | private String detail; // 상세 내용 62 | 63 | @Column(nullable = false) 64 | private boolean isGraduating; // 졸업 예정 여부 65 | 66 | public void updateViewCount(){ 67 | this.viewCount += 1; 68 | } 69 | 70 | public void updateScrapCount(int plusOrMinus){ 71 | this.scrapCount = scrapCount + plusOrMinus; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/logging/LoggingFilter.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.logging; 2 | 3 | import jakarta.servlet.FilterChain; 4 | import jakarta.servlet.ServletException; 5 | import jakarta.servlet.http.HttpServletRequest; 6 | import jakarta.servlet.http.HttpServletResponse; 7 | 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.slf4j.MDC; 10 | 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.web.filter.OncePerRequestFilter; 13 | 14 | import java.io.IOException; 15 | import java.util.Map; 16 | import java.util.UUID; 17 | import java.util.stream.Collectors; 18 | 19 | @Component 20 | @Slf4j 21 | public class LoggingFilter extends OncePerRequestFilter { 22 | 23 | private static final String TRACE_ID = "traceId"; 24 | private static final String X_FORWARDED_FOR_HEADER = "X-FORWARDED-FOR"; 25 | 26 | @Override 27 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 28 | request = new CachedHttpServletRequest(request); 29 | 30 | MDC.put(TRACE_ID, UUID.randomUUID().toString()); 31 | 32 | String requestLine = request.getMethod() + " " + request.getRequestURI() + getRequestParams(request); 33 | log.info("Request ({}): {} {}", getRequestAddr(request), requestLine, request.getReader().lines().collect(Collectors.joining())); 34 | 35 | filterChain.doFilter(request, response); 36 | 37 | log.info("Response: {} : {}", requestLine, response.getStatus()); 38 | } 39 | 40 | private String getRequestAddr(HttpServletRequest request) { 41 | String requestAddr = request.getHeader(X_FORWARDED_FOR_HEADER); 42 | return requestAddr != null ? requestAddr : request.getRemoteAddr(); 43 | } 44 | 45 | private String getRequestParams(HttpServletRequest request) { 46 | Map paramMap = request.getParameterMap(); 47 | 48 | if (paramMap.isEmpty()) { 49 | return ""; 50 | } 51 | 52 | String queryParameters = paramMap.entrySet().stream() 53 | .map(entry -> entry.getKey() + "=" + (entry.getValue().length > 0 ? entry.getValue()[0] : "")) 54 | .collect(Collectors.joining("&")); 55 | 56 | return "?" + queryParameters; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/search/dto/response/SearchResultResponseDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.search.dto.response; 2 | 3 | import lombok.Builder; 4 | import org.terning.terningserver.internshipAnnouncement.domain.InternshipAnnouncement; 5 | import org.terning.terningserver.common.util.DateUtil; 6 | 7 | import java.util.List; 8 | 9 | import static lombok.AccessLevel.PRIVATE; 10 | 11 | public record SearchResultResponseDto( 12 | int totalPages, 13 | long totalCount, 14 | Boolean hasNext, 15 | List announcements 16 | ) { 17 | @Builder(access = PRIVATE) 18 | public record SearchAnnouncementResponse( 19 | long internshipAnnouncementId, 20 | String companyImage, 21 | String dDay, 22 | String title, 23 | String workingPeriod, 24 | boolean isScrapped, 25 | String color, 26 | String deadline, 27 | String startYearMonth 28 | 29 | ) { 30 | public static SearchAnnouncementResponse from( 31 | final InternshipAnnouncement announcement, 32 | final Long scrapId, 33 | final String color 34 | ) { 35 | return SearchAnnouncementResponse.builder() 36 | .internshipAnnouncementId(announcement.getId()) 37 | .companyImage(announcement.getCompany().getCompanyImage()) 38 | .dDay(DateUtil.convert(announcement.getDeadline())) 39 | .title(announcement.getTitle()) 40 | .workingPeriod(announcement.getWorkingPeriod()) 41 | .isScrapped(scrapId!=null) 42 | .color(color) 43 | .deadline(DateUtil.convertDeadline(announcement.getDeadline())) 44 | .startYearMonth(announcement.getStartYear() + "년 " + announcement.getStartMonth() + "월") 45 | .build(); 46 | } 47 | } 48 | public static SearchResultResponseDto of( 49 | final int totalPages, 50 | final long totalCount, 51 | final Boolean hasNext, 52 | final List announcements 53 | ) { 54 | return new SearchResultResponseDto(totalPages, totalCount, hasNext, announcements); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/internshipAnnouncement/dto/response/InternshipDetailResponseDto.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.internshipAnnouncement.dto.response; 2 | 3 | import lombok.Builder; 4 | import org.terning.terningserver.internshipAnnouncement.domain.Company; 5 | import org.terning.terningserver.internshipAnnouncement.domain.InternshipAnnouncement; 6 | import org.terning.terningserver.common.util.DateUtil; 7 | 8 | import static lombok.AccessLevel.PRIVATE; 9 | 10 | @Builder(access = PRIVATE) 11 | public record InternshipDetailResponseDto( 12 | String companyImage, 13 | String dDay, 14 | String title, 15 | String workingPeriod, 16 | boolean isScrapped, 17 | String color, 18 | String deadline, 19 | String startYearMonth, 20 | int scrapCount, 21 | int viewCount, 22 | String company, 23 | String companyCategory, 24 | String qualification, 25 | String jobType, 26 | String detail, 27 | String url 28 | ) { 29 | public static InternshipDetailResponseDto of( 30 | final InternshipAnnouncement announcement, 31 | final Company company, 32 | final Long scrapId, 33 | final String color 34 | ) { 35 | return InternshipDetailResponseDto.builder() 36 | .companyImage(company.getCompanyImage()) 37 | .dDay(DateUtil.convert(announcement.getDeadline())) 38 | .title(announcement.getTitle()) 39 | .workingPeriod(announcement.getWorkingPeriod()) 40 | .isScrapped(scrapId!=null) 41 | .color(color) 42 | .deadline(DateUtil.convertDeadline(announcement.getDeadline())) 43 | .startYearMonth(announcement.getStartYear() + "년 " + announcement.getStartMonth() + "월") 44 | .scrapCount(announcement.getScrapCount()) 45 | .viewCount(announcement.getViewCount()) 46 | .company(company.getCompanyInfo()) 47 | .companyCategory(company.getCompanyCategory().getValue()) 48 | .qualification(announcement.getQualifications()) 49 | .jobType(announcement.getJobType()) 50 | .detail(announcement.getDetail()) 51 | .url(announcement.getUrl()) 52 | .build(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/exception/enums/SuccessMessage.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.exception.enums; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | 6 | @Getter 7 | @AllArgsConstructor 8 | public enum SuccessMessage { 9 | 10 | // 홈 화면 11 | SUCCESS_GET_ANNOUNCEMENTS(200, "인턴 공고 불러오기를 성공했습니다"), 12 | SUCCESS_GET_UPCOMING_ANNOUNCEMENTS(200, "곧 마감인 인턴 공고 요청을 성공했습니다"), 13 | SUCCESS_GET_UPCOMING_ANNOUNCEMENTS_NO_SCRAP(200, "아직 스크랩된 인턴 공고가 없어요!"), 14 | SUCCESS_GET_UPCOMING_ANNOUNCEMENTS_EMPTY_LIST(200, "일주일 내에 마감인 공고가 없어요\n캘린더에서 스크랩한 공고 일정을 확인해 보세요"), 15 | 16 | // 토큰 재발급 17 | SUCCESS_REISSUE_TOKEN(200, "토큰 재발급에 성공하였습니다."), 18 | 19 | // 회원가입 20 | SUCCESS_SIGN_UP(201, "회원가입에 성공하였습니다"), 21 | 22 | // 사용자 정보 필터링 23 | SUCCESS_SIGN_UP_FILTER(200, "사용자 필터링 정보 생성에 성공하였습니다"), 24 | 25 | // 로그 아웃 26 | SUCCESS_SIGN_OUT(200, "로그아웃에 성공하였습니다"), 27 | SUCCESS_REFRESH_TOKEN_RESET(200, "리프레쉬 토큰 초기화에 성공하였습니다"), 28 | 29 | // 계정 탈퇴 30 | SUCCESS_WITHDRAW(200, "계정 탈퇴에 성공하였습니다"), 31 | 32 | // Search (탐색 화면) 33 | SUCCESS_GET_MOST_VIEWED_ANNOUNCEMENTS(200, "탐색 > 조회수 많은 공고를 조회하는데 성공했습니다"), 34 | SUCCESS_GET_MOST_SCRAPPED_ANNOUNCEMENTS(200, "탐색 > 스크랩 수 많은 공고를 조회하는데 성공했습니다"), 35 | SUCCESS_GET_SEARCH_ANNOUNCEMENTS(200, "검색에 성공했습니다"), 36 | SUCCESS_GET_BANNERS(200, "탐색 뷰 > 배너 조회에 성공했습니다"), 37 | 38 | // 인턴 공고 39 | SUCCESS_GET_INTERNSHIP_DETAIL(200, "공고 상세 정보 불러오기에 성공했습니다"), 40 | 41 | // 스크랩 42 | SUCCESS_CREATE_SCRAP(201, "스크랩 추가에 성공했습니다"), 43 | SUCCESS_DELETE_SCRAP(200, "스크랩 취소에 성공했습니다"), 44 | SUCCESS_UPDATE_SCRAP(200, "스크랩 수정에 성공했습니다"), 45 | 46 | // Calendar (캘린더 화면) 47 | SUCCESS_GET_MONTHLY_SCRAPS(200, "캘린더 > (월간) 스크랩 된 공고 정보 불러오기를 성공했습니다"), 48 | SUCCESS_GET_MONTHLY_SCRAPS_AS_LIST(200, "캘린더 > (월간) 스크랩 된 공고 정보 (리스트) 불러오기를 성공했습니다"), 49 | SUCCESS_GET_DAILY_SCRAPS(200, "캘린더 > (일간) 스크랩 된 공고 정보 불러오기를 성공했습니다"), 50 | 51 | // Filter(필터링) 52 | SUCCESS_GET_USER_FILTER(200, "사용자의 필터링 정보를 불러오는데 성공했습니다"), 53 | SUCCESS_UPDATE_USER_FILTER(200, "필터링 재설정에 성공했습니다"), 54 | 55 | // My page (마이페이지 화면) 56 | SUCCESS_GET_PROFILE(200, "마이페이지 > 프로필 정보 불러오기를 성공했습니다"), 57 | SUCCESS_UPDATE_PROFILE(200, "프로필 수정에 성공했습니다"), 58 | PUSH_STATUS_UPDATED(200, "사용자 푸시알림 여부 변경을 완료했습니다."), 59 | 60 | // 유저 동기화 61 | SUCCESS_USER_SYNC(201, "유저 동기화를 성공했습니다."); 62 | 63 | private final int status; 64 | private final String message; 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/discord/application/WebhookService.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.discord.application; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.event.EventListener; 6 | import org.springframework.http.HttpEntity; 7 | import org.springframework.http.HttpHeaders; 8 | import org.springframework.http.MediaType; 9 | import org.springframework.stereotype.Service; 10 | import org.springframework.transaction.annotation.Transactional; 11 | import org.springframework.web.client.RestTemplate; 12 | import org.terning.terningserver.user.domain.User; 13 | import org.terning.terningserver.user.event.UserSignedUpEvent; 14 | import org.terning.terningserver.user.repository.UserRepository; 15 | 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | @RequiredArgsConstructor 20 | @Service 21 | @Transactional 22 | public class WebhookService { 23 | 24 | // 디스코드 웹훅 URL 주입 25 | @Value("${discord.webhook.url}") 26 | private String discordWebhookUrl; 27 | 28 | private final UserRepository userRepository; 29 | 30 | @EventListener 31 | public void handleUserSignedUpEvent(UserSignedUpEvent event) { 32 | sendDiscordNotification(event.user()); 33 | } 34 | 35 | 36 | // 알림을 보내는 메서드 37 | private void sendDiscordNotification(User user) { 38 | 39 | // discord.webhook.url 값이 비어있으면 웹훅을 실행하지 않음 40 | if (discordWebhookUrl == null || discordWebhookUrl.isEmpty()) { 41 | // 스테이징 환경에서는 웹훅을 비활성화 42 | return; 43 | } 44 | 45 | // REST 요청을 처리하기 위한 RestTemplate 객체 생성 46 | RestTemplate restTemplate = new RestTemplate(); 47 | 48 | // 회원 수를 기존 DB에서 조회하여 총 회원 수 계산 49 | Long totalMembers = userRepository.count(); 50 | 51 | // 알림 메시지 생성 52 | String message = String.format("가입자명 : %s\n로그인방식: %s\n[%d] 번째 유저가 회원가입했습니다!", user.getName(), user.getAuthType(), totalMembers); 53 | 54 | // HTTP 요청을 위한 헤더 설정 55 | HttpHeaders headers = new HttpHeaders(); 56 | headers.setContentType(MediaType.APPLICATION_JSON); 57 | 58 | // HTTP 요청 바디에 전송할 메시지 설정 59 | Map body = new HashMap<>(); 60 | body.put("content", message); 61 | 62 | // HTTP 요청 엔터티 생성 63 | HttpEntity> requestEntity = new HttpEntity<>(body, headers); 64 | 65 | // 디스코드 웹훅 URL로 POST 요청을 보내어 알림 전송 66 | restTemplate.postForEntity(discordWebhookUrl, requestEntity, String.class); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/DOCKER-CD.yml: -------------------------------------------------------------------------------- 1 | name: DOCKER-CD 2 | on: 3 | push: 4 | branches: [ "main" ] 5 | 6 | jobs: 7 | ci: 8 | runs-on: ubuntu-24.04 9 | env: 10 | working-directory: . 11 | 12 | 13 | steps: 14 | - name: 체크아웃 15 | uses: actions/checkout@v3 16 | 17 | - name: Set up JDK 21 18 | uses: actions/setup-java@v3 19 | with: 20 | distribution: 'temurin' 21 | java-version: '21' 22 | 23 | # gradle caching - 빌드 시간 향상 24 | - name: Gradle Caching (빌드 시간 향상) 25 | uses: actions/cache@v3 26 | with: 27 | path: | 28 | ~/.gradle/caches 29 | ~/.gradle/wrapper 30 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 31 | restore-keys: | 32 | ${{ runner.os }}-gradle- 33 | 34 | - name: application.yml 생성 35 | run: | 36 | mkdir -p ./src/main/resources && cd $_ 37 | touch ./application.yml 38 | echo "${{ secrets.YML }}" > ./application.yml 39 | cat ./application.yml 40 | working-directory: ${{ env.working-directory }} 41 | 42 | - name: application-prod.yml 생성 43 | run: | 44 | cd ./src/main/resources 45 | touch ./application-prod.yml 46 | echo "${{ secrets.YML_PROD }}" > ./application-prod.yml 47 | working-directory: ${{ env.working-directory }} 48 | 49 | - name: 빌드 50 | run: | 51 | chmod +x gradlew 52 | ./gradlew build -x test 53 | working-directory: ${{ env.working-directory }} 54 | shell: bash 55 | 56 | 57 | - name: docker 로그인 58 | uses: docker/setup-buildx-action@v2.9.1 59 | 60 | - name: login docker hub 61 | uses: docker/login-action@v2.2.0 62 | with: 63 | username: ${{ secrets.DOCKER_LOGIN_USERNAME }} 64 | password: ${{ secrets.DOCKER_LOGIN_ACCESSTOKEN }} 65 | 66 | - name: docker image 빌드 및 푸시 67 | run: | 68 | docker build --platform linux/amd64 -t terningpoint/terning2025 . 69 | docker push terningpoint/terning2025 70 | working-directory: ${{ env.working-directory }} 71 | 72 | cd: 73 | needs: ci 74 | runs-on: ubuntu-24.04 75 | 76 | steps: 77 | - name: docker 컨테이너 실행 78 | uses: appleboy/ssh-action@master 79 | with: 80 | host: ${{ secrets.SERVER_IP }} 81 | username: ${{ secrets.SERVER_USER }} 82 | key: ${{ secrets.SERVER_KEY }} 83 | script: | 84 | cd ~ 85 | ./deploy.sh 86 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/search/api/SearchController.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.search.api; 2 | 3 | 4 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_GET_MOST_SCRAPPED_ANNOUNCEMENTS; 5 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_GET_MOST_VIEWED_ANNOUNCEMENTS; 6 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_GET_SEARCH_ANNOUNCEMENTS; 7 | 8 | import lombok.RequiredArgsConstructor; 9 | import org.springframework.data.domain.Pageable; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestParam; 14 | import org.springframework.web.bind.annotation.RestController; 15 | import org.terning.terningserver.auth.config.Login; 16 | import org.terning.terningserver.search.dto.response.PopularAnnouncementListResponseDto; 17 | import org.terning.terningserver.search.dto.response.SearchResultResponseDto; 18 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 19 | import org.terning.terningserver.search.application.SearchService; 20 | 21 | 22 | @RestController 23 | @RequiredArgsConstructor 24 | @RequestMapping("/api/v1") 25 | public class SearchController implements SearchSwagger { 26 | 27 | private final SearchService searchService; 28 | 29 | @GetMapping("/search/views") 30 | public ResponseEntity> getMostViewedAnnouncements() { 31 | return ResponseEntity.ok(SuccessResponse.of( 32 | SUCCESS_GET_MOST_VIEWED_ANNOUNCEMENTS, 33 | searchService.getMostViewedAnnouncements() 34 | )); 35 | } 36 | 37 | @GetMapping("/search/scraps") 38 | public ResponseEntity> getMostScrappedAnnouncements() { 39 | return ResponseEntity.ok(SuccessResponse.of( 40 | SUCCESS_GET_MOST_SCRAPPED_ANNOUNCEMENTS, 41 | searchService.getMostScrappedAnnouncements() 42 | )); 43 | } 44 | 45 | @GetMapping("/search") 46 | public ResponseEntity> searchInternshipAnnouncement( 47 | @Login Long userId, 48 | @RequestParam(value = "keyword", required = false) String keyword, 49 | @RequestParam(value = "sortBy", required = false) String sortBy, Pageable pageable) { 50 | return ResponseEntity.ok(SuccessResponse.of( 51 | SUCCESS_GET_SEARCH_ANNOUNCEMENTS, 52 | searchService.searchInternshipAnnouncement(keyword, sortBy, pageable, userId) 53 | )); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/common/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.common.config; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.method.support.HandlerMethodArgumentResolver; 6 | import org.springframework.web.servlet.config.annotation.CorsRegistry; 7 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 8 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 9 | import org.terning.terningserver.auth.config.LoginCheckInterceptor; 10 | import org.terning.terningserver.auth.config.LoginUserArgumentResolver; 11 | 12 | import java.util.List; 13 | 14 | @Configuration 15 | @RequiredArgsConstructor 16 | public class WebConfig implements WebMvcConfigurer { 17 | 18 | private final LoginCheckInterceptor loginCheckInterceptor; 19 | private final LoginUserArgumentResolver loginUserArgumentResolver; 20 | 21 | private static final String[] AUTH_WHITELIST = { 22 | "/v3/api-docs/**", 23 | "/swagger-ui.html", 24 | "/swagger-resources/**", 25 | "/swagger-ui/**", 26 | 27 | "/api/v1/auth/sign-in", 28 | "/api/v1/auth/sign-up", 29 | "/api/v1/auth/sign-up/filter", 30 | "/api/v1/auth/token-reissue", 31 | 32 | "/api/v1/search/banners", 33 | "/api/v1/search/views", 34 | "/api/v1/search/scraps", 35 | 36 | "/actuator/health", 37 | "/api/v1/external/scraps/unsynced", 38 | "/api/v1/external/scraps/sync/result", 39 | 40 | "/api/v1/users" 41 | }; 42 | 43 | @Override 44 | public void addCorsMappings(CorsRegistry registry) { 45 | registry.addMapping("/**") 46 | .allowedOrigins( 47 | "http://localhost:8080", 48 | "http://localhost:3000", 49 | "https://www.terning-official.p-e.kr/", 50 | "https://www.terning-official.n-e.kr/", 51 | "http://15.165.242.132", 52 | "http://54.180.215.35") 53 | .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH") 54 | .allowCredentials(true); 55 | } 56 | 57 | @Override 58 | public void addInterceptors(InterceptorRegistry registry) { 59 | registry.addInterceptor(loginCheckInterceptor) 60 | .order(1) 61 | .addPathPatterns("/api/v1/**") 62 | .excludePathPatterns(AUTH_WHITELIST); 63 | } 64 | 65 | @Override 66 | public void addArgumentResolvers(List resolvers) { 67 | resolvers.add(loginUserArgumentResolver); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/api/UserProfileController.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.api; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PatchMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RestController; 10 | import org.terning.terningserver.auth.config.Login; 11 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 12 | import org.terning.terningserver.common.exception.enums.SuccessMessage; 13 | import org.terning.terningserver.external.pushNotification.notification.NotificationUserClient; 14 | import org.terning.terningserver.user.application.UserService; 15 | import org.terning.terningserver.user.dto.request.ProfileUpdateRequestDto; 16 | import org.terning.terningserver.user.dto.request.PushStatusUpdateRequest; 17 | import org.terning.terningserver.user.dto.response.ProfileResponseDto; 18 | 19 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_GET_PROFILE; 20 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_UPDATE_PROFILE; 21 | 22 | @RestController 23 | @RequiredArgsConstructor 24 | @RequestMapping("/api/v1") 25 | public class UserProfileController implements UserSwagger { 26 | 27 | private final UserService userService; 28 | private final NotificationUserClient notificationUserClient; 29 | 30 | @GetMapping("/mypage/profile") 31 | public ResponseEntity> getProfile( 32 | @Login Long userId 33 | ){ 34 | ProfileResponseDto profile = userService.getProfile(userId); 35 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_GET_PROFILE, profile)); 36 | } 37 | 38 | @PatchMapping("/mypage/profile") 39 | public ResponseEntity updateProfile( 40 | @Login Long userId, 41 | @RequestBody ProfileUpdateRequestDto request 42 | ){ 43 | userService.updateProfile(userId, request); 44 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_UPDATE_PROFILE)); 45 | } 46 | 47 | @PatchMapping("/push-status") 48 | public ResponseEntity> updatePushStatus( 49 | @Login Long userId, 50 | @RequestBody PushStatusUpdateRequest request 51 | ) { 52 | userService.updatePushStatus(userId, request.newStatus()); 53 | 54 | notificationUserClient.updatePushStatus(userId, request.newStatus()); 55 | 56 | return ResponseEntity.ok(SuccessResponse.of(SuccessMessage.PUSH_STATUS_UPDATED)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/scrap/api/ScrapController.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.scrap.api; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.DeleteMapping; 6 | import org.springframework.web.bind.annotation.PatchMapping; 7 | import org.springframework.web.bind.annotation.PathVariable; 8 | import org.springframework.web.bind.annotation.PostMapping; 9 | import org.springframework.web.bind.annotation.RequestBody; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | import org.springframework.web.bind.annotation.RestController; 12 | import org.terning.terningserver.auth.config.Login; 13 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 14 | import org.terning.terningserver.scrap.application.ScrapService; 15 | import org.terning.terningserver.scrap.dto.request.CreateScrapRequestDto; 16 | import org.terning.terningserver.scrap.dto.request.UpdateScrapRequestDto; 17 | 18 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_CREATE_SCRAP; 19 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_DELETE_SCRAP; 20 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_UPDATE_SCRAP; 21 | 22 | @RestController 23 | @RequiredArgsConstructor 24 | @RequestMapping("/api/v1") 25 | public class ScrapController implements ScrapSwagger { 26 | 27 | private final ScrapService scrapService; 28 | 29 | @PostMapping("/scraps/{internshipAnnouncementId}") 30 | public ResponseEntity createScrap( 31 | @Login long userId, 32 | @PathVariable long internshipAnnouncementId, 33 | @RequestBody CreateScrapRequestDto request) { 34 | scrapService.createScrap(internshipAnnouncementId, request, userId); 35 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_CREATE_SCRAP)); 36 | } 37 | 38 | @DeleteMapping("/scraps/{internshipAnnouncementId}") 39 | public ResponseEntity deleteScrap( 40 | @Login long userId, 41 | @PathVariable long internshipAnnouncementId) { 42 | scrapService.deleteScrap(internshipAnnouncementId, userId); 43 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_DELETE_SCRAP)); 44 | } 45 | 46 | @PatchMapping("/scraps/{internshipAnnouncementId}") 47 | public ResponseEntity updateScrapColor( 48 | @Login long userId, 49 | @PathVariable long internshipAnnouncementId, 50 | @RequestBody UpdateScrapRequestDto request) { 51 | scrapService.updateScrapColor(internshipAnnouncementId, request, userId); 52 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_UPDATE_SCRAP)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/user/config/NotificationSyncTasklet.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.user.config; 2 | 3 | import java.util.List; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.batch.core.StepContribution; 7 | import org.springframework.batch.core.scope.context.ChunkContext; 8 | import org.springframework.batch.core.step.tasklet.Tasklet; 9 | import org.springframework.batch.repeat.RepeatStatus; 10 | import org.springframework.stereotype.Component; 11 | import org.terning.terningserver.external.pushNotification.notification.NotificationUserClient; 12 | import org.terning.terningserver.external.pushNotification.user.domain.UserSyncEvent; 13 | import org.terning.terningserver.external.pushNotification.user.respository.UserSyncEventRepository; 14 | 15 | @Component 16 | @RequiredArgsConstructor 17 | @Slf4j 18 | public class NotificationSyncTasklet implements Tasklet { 19 | 20 | private final UserSyncEventRepository eventRepository; 21 | private final NotificationUserClient notificationUserClient; 22 | 23 | private static final String NO_SYNC_EVENTS = "Unsync 상태의 유저 event가 없습니다."; 24 | private static final String COMPLETED_SYNC_EVENTS = "유저 이벤트 동기화에 성공했습니다."; 25 | private static final String FAILED_SYNC_EVENTS = "유저 이벤트 동기화에 성공했습니다."; 26 | 27 | @Override 28 | public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) { 29 | List events = eventRepository.findByProcessedFalse(); 30 | if (events.isEmpty()) { 31 | log.info(NO_SYNC_EVENTS); 32 | return RepeatStatus.FINISHED; 33 | } 34 | 35 | for (UserSyncEvent event : events) { 36 | try { 37 | Long userId = event.getUserId(); 38 | switch (event.getEventType()) { 39 | case PUSH_STATUS_CHANGE: 40 | notificationUserClient.updatePushStatus(userId, event.getNewValue()); 41 | break; 42 | case NAME_CHANGE: 43 | notificationUserClient.updateUserName(userId, event.getNewValue()); 44 | break; 45 | case WITHDRAW: 46 | notificationUserClient.deleteUser(userId); 47 | break; 48 | } 49 | event.markProcessed(); 50 | eventRepository.save(event); 51 | log.info(COMPLETED_SYNC_EVENTS + " Id = " + event.getId() + " userId = " + event.getUserId()); 52 | } catch (Exception e) { 53 | log.error(FAILED_SYNC_EVENTS + " Id = " + event.getId() + " userId = " + event.getUserId()); 54 | } 55 | } 56 | return RepeatStatus.FINISHED; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/DOCKER-CD-STAGING.yml: -------------------------------------------------------------------------------- 1 | name: DOCKER-CD-STAGING 2 | 3 | on: 4 | push: 5 | branches: [ "staging" ] 6 | 7 | jobs: 8 | ci: 9 | # Using Environment - Staging 환경 사용 10 | # environment: staging.. 11 | runs-on: ubuntu-24.04 12 | env: 13 | working-directory: . 14 | 15 | # Checkout - 가상 머신에 체크아웃 16 | steps: 17 | - name: 체크아웃 18 | uses: actions/checkout@v3 19 | 20 | # JDK setting - JDK 21 설정 21 | - name: Set up JDK 21 22 | uses: actions/setup-java@v3 23 | with: 24 | distribution: 'temurin' 25 | java-version: '21' 26 | 27 | # Gradle caching - 빌드 시간 향상 28 | - name: Gradle Caching 29 | uses: actions/cache@v3 30 | with: 31 | path: | 32 | ~/.gradle/caches 33 | ~/.gradle/wrapper 34 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} 35 | restore-keys: | 36 | ${{ runner.os }}-gradle- 37 | 38 | # create .yml - yml 파일 생성 39 | - name: application.yml 생성 40 | run: | 41 | mkdir -p ./src/main/resources && cd $_ 42 | touch ./application.yml 43 | echo "${{ secrets.YML }}" > ./application.yml 44 | cat ./application.yml 45 | working-directory: ${{ env.working-directory }} 46 | 47 | - name: application-staging.yml 생성 48 | run: | 49 | cd ./src/main/resources 50 | touch ./application-staging.yml 51 | echo "${{ secrets.YML_STAGING }}" > ./application-staging.yml 52 | working-directory: ${{ env.working-directory }} 53 | 54 | # Gradle build - 테스트 없이 gradle 빌드 55 | - name: 빌드 56 | run: | 57 | chmod +x gradlew 58 | ./gradlew build -x test 59 | working-directory: ${{ env.working-directory }} 60 | shell: bash 61 | 62 | - name: docker 로그인 63 | uses: docker/setup-buildx-action@v2.9.1 64 | 65 | - name: login docker hub 66 | uses: docker/login-action@v2.2.0 67 | with: 68 | username: ${{ secrets.DOCKER_LOGIN_USERNAME }} 69 | password: ${{ secrets.DOCKER_LOGIN_ACCESSTOKEN }} 70 | 71 | - name: docker image 빌드 및 푸시 72 | run: | 73 | docker build -f Dockerfile-staging --platform linux/amd64 -t terningpoint/terning2025-staging . 74 | docker push terningpoint/terning2025-staging 75 | working-directory: ${{ env.working-directory }} 76 | 77 | cd: 78 | needs: ci 79 | runs-on: ubuntu-24.04 80 | 81 | steps: 82 | - name: Debugging - Echo Host 83 | run: echo "${{ secrets.STAGING_SERVER_IP }}" 84 | 85 | - name: docker 컨테이너 실행 86 | uses: appleboy/ssh-action@master 87 | with: 88 | host: ${{ secrets.STAGING_SERVER_IP }} 89 | username: ${{ secrets.STAGING_SERVER_USER }} 90 | key: ${{ secrets.STAGING_SERVER_KEY }} 91 | script: | 92 | cd ~ 93 | ./deploy-staging.sh 94 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/calendar/api/CalendarController.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.calendar.api; 2 | 3 | import org.terning.terningserver.auth.config.Login; 4 | import org.terning.terningserver.calendar.dto.response.DailyScrapResponseDto; 5 | import org.terning.terningserver.calendar.dto.response.MonthlyDefaultResponseDto; 6 | import org.terning.terningserver.calendar.dto.response.MonthlyListResponseDto; 7 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 8 | import org.terning.terningserver.scrap.application.ScrapService; 9 | import lombok.RequiredArgsConstructor; 10 | import org.springframework.http.ResponseEntity; 11 | import org.springframework.web.bind.annotation.GetMapping; 12 | import org.springframework.web.bind.annotation.RequestMapping; 13 | import org.springframework.web.bind.annotation.RequestParam; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | import java.time.LocalDate; 17 | import java.util.List; 18 | 19 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_GET_DAILY_SCRAPS; 20 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_GET_MONTHLY_SCRAPS; 21 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_GET_MONTHLY_SCRAPS_AS_LIST; 22 | 23 | 24 | @RestController 25 | @RequiredArgsConstructor 26 | @RequestMapping("/api/v1") 27 | public class CalendarController implements CalendarSwagger { 28 | 29 | private final ScrapService scrapService; 30 | 31 | @GetMapping("/calendar/monthly-default") 32 | public ResponseEntity>> getMonthlyScraps( 33 | @Login Long userId, 34 | @RequestParam("year") int year, 35 | @RequestParam("month") int month 36 | ){ 37 | List monthlyScraps = scrapService.getMonthlyScraps(userId, year, month); 38 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_GET_MONTHLY_SCRAPS, monthlyScraps)); 39 | } 40 | 41 | @GetMapping("/calendar/monthly-list") 42 | public ResponseEntity>> getMonthlyScrapsAsList( 43 | @Login Long userId, 44 | @RequestParam("year") int year, 45 | @RequestParam("month") int month 46 | ){ 47 | List monthlyScraps = scrapService.getMonthlyScrapsAsList(userId, year, month); 48 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_GET_MONTHLY_SCRAPS_AS_LIST, monthlyScraps)); 49 | } 50 | 51 | @GetMapping("/calendar/daily") 52 | public ResponseEntity>> getDailyScraps( 53 | @Login Long userId, 54 | @RequestParam("date") String date 55 | ){ 56 | LocalDate localDate = LocalDate.parse(date); 57 | List dailyScraps = scrapService.getDailyScraps(userId, localDate); 58 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_GET_DAILY_SCRAPS, dailyScraps)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/api/AuthSwagger.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.api; 2 | 3 | import io.swagger.v3.oas.annotations.Operation; 4 | import io.swagger.v3.oas.annotations.Parameter; 5 | import io.swagger.v3.oas.annotations.tags.Tag; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.web.bind.annotation.RequestHeader; 9 | import org.terning.terningserver.auth.config.Login; 10 | import org.terning.terningserver.auth.dto.request.FcmTokenSyncRequest; 11 | import org.terning.terningserver.auth.dto.request.SignInRequest; 12 | import org.terning.terningserver.auth.dto.request.SignUpFilterRequest; 13 | import org.terning.terningserver.auth.dto.request.SignUpRequest; 14 | import org.terning.terningserver.auth.dto.response.SignInResponse; 15 | import org.terning.terningserver.auth.dto.response.SignUpResponse; 16 | import org.terning.terningserver.auth.dto.response.TokenReissueResponse; 17 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 18 | 19 | @Tag(name = "Auth", description = "소셜 로그인 및 회원가입 API") 20 | public interface AuthSwagger { 21 | 22 | @Operation(summary = "소셜 로그인", description = "AuthType에 맞는 소셜 로그인 API") 23 | ResponseEntity> signIn( 24 | @Parameter(name = "Authorization", description = "", example = "authAccessToken") 25 | @RequestHeader("Authorization") String authAccessToken, 26 | @RequestBody SignInRequest request 27 | ); 28 | 29 | @Operation(summary = "토큰 재발급", description = "토큰 재발급 API") 30 | ResponseEntity> reissueToken( 31 | @Parameter(name = "Authorization", description = "", example = "refreshToken") 32 | @RequestHeader("Authorization") String refreshToken 33 | ); 34 | 35 | @Operation(summary = "사용자 필터링 정보 생성", description = "사용자 필터링 정보 생성 API") 36 | ResponseEntity registerUserFilter( 37 | @Parameter(name = "User-Id", description = "", example = "userId") 38 | @RequestHeader("User-Id") Long userId, 39 | @RequestBody SignUpFilterRequest request 40 | ); 41 | 42 | @Operation(summary = "회원가입", description = "회원가입 API") 43 | ResponseEntity> signUp( 44 | @Parameter(name = "Authorization", description = "", example = "authId") 45 | @RequestHeader("authId") String authId, 46 | @RequestBody SignUpRequest request 47 | ); 48 | 49 | @Operation(summary = "로그아웃", description = "로그아웃 API") 50 | ResponseEntity signOut( 51 | @Parameter(hidden = true) @Login Long userId); 52 | 53 | @Operation(summary = "계정탈퇴", description = "계정탈퇴 API") 54 | ResponseEntity withdraw( 55 | @Parameter(hidden = true) @Login Long userId); 56 | 57 | @Operation(summary = "유저동기화", description = "유저동기화 API") 58 | ResponseEntity syncUser( 59 | @Parameter(hidden = true) @Login Long userId, 60 | @RequestBody FcmTokenSyncRequest request 61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/home/api/HomeController.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.home.api; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.web.PageableDefault; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestParam; 10 | import org.springframework.web.bind.annotation.RestController; 11 | import org.terning.terningserver.auth.config.Login; 12 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 13 | import org.terning.terningserver.home.application.HomeService; 14 | import org.terning.terningserver.home.dto.response.HomeAnnouncementsResponseDto; 15 | import org.terning.terningserver.home.dto.response.UpcomingScrapResponseDto; 16 | import org.terning.terningserver.scrap.application.ScrapService; 17 | 18 | import java.util.List; 19 | 20 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_GET_ANNOUNCEMENTS; 21 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_GET_UPCOMING_ANNOUNCEMENTS; 22 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_GET_UPCOMING_ANNOUNCEMENTS_EMPTY_LIST; 23 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_GET_UPCOMING_ANNOUNCEMENTS_NO_SCRAP; 24 | 25 | @RestController 26 | @RequiredArgsConstructor 27 | @RequestMapping("/api/v1") 28 | public class HomeController implements HomeSwagger { 29 | 30 | private final HomeService homeService; 31 | private final ScrapService scrapService; 32 | 33 | @GetMapping("/home") 34 | public ResponseEntity> getAnnouncements( 35 | @Login Long userId, 36 | @RequestParam(value = "sortBy", required = false, defaultValue = "deadlineSoon") String sortBy, 37 | @PageableDefault(size = 10) Pageable pageable) { 38 | HomeAnnouncementsResponseDto announcements = homeService.getAnnouncements(userId, sortBy, pageable); 39 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_GET_ANNOUNCEMENTS, announcements)); 40 | } 41 | 42 | 43 | @GetMapping("/home/upcoming") 44 | public ResponseEntity>> getUpcomingScraps( 45 | @Login Long userId 46 | ){ 47 | 48 | boolean hasScrapped = scrapService.hasUserScrapped(userId); 49 | List scrapList = scrapService.getUpcomingScrap(userId); 50 | 51 | UpcomingScrapResponseDto responseDto = new UpcomingScrapResponseDto(hasScrapped, scrapList); 52 | 53 | if(!hasScrapped){ 54 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_GET_UPCOMING_ANNOUNCEMENTS_NO_SCRAP, responseDto)); 55 | } else if (scrapList.isEmpty()) { 56 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_GET_UPCOMING_ANNOUNCEMENTS_EMPTY_LIST, responseDto)); 57 | } else { 58 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_GET_UPCOMING_ANNOUNCEMENTS, responseDto)); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/search/application/SearchServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.search.application; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.data.domain.Page; 5 | import org.springframework.data.domain.Pageable; 6 | import org.springframework.stereotype.Service; 7 | import org.springframework.transaction.annotation.Transactional; 8 | import org.terning.terningserver.internshipAnnouncement.domain.InternshipAnnouncement; 9 | import org.terning.terningserver.scrap.domain.Scrap; 10 | import org.terning.terningserver.search.dto.response.PopularAnnouncementListResponseDto; 11 | import org.terning.terningserver.search.dto.response.SearchResultResponseDto; 12 | import org.terning.terningserver.internshipAnnouncement.repository.InternshipRepository; 13 | import org.terning.terningserver.scrap.repository.ScrapRepository; 14 | 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.stream.Collectors; 18 | 19 | @Service 20 | @RequiredArgsConstructor 21 | @Transactional(readOnly = true) 22 | public class SearchServiceImpl implements SearchService { 23 | 24 | private final InternshipRepository internshipRepository; 25 | private final ScrapRepository scrapRepository; 26 | 27 | @Override 28 | public PopularAnnouncementListResponseDto getMostViewedAnnouncements() { 29 | List mostViewedInternships = internshipRepository.getMostViewedInternship(); 30 | return PopularAnnouncementListResponseDto.of(mostViewedInternships); 31 | } 32 | 33 | @Override 34 | public PopularAnnouncementListResponseDto getMostScrappedAnnouncements() { 35 | List mostViewedInternships = internshipRepository.getMostScrappedInternship(); 36 | return PopularAnnouncementListResponseDto.of(mostViewedInternships); 37 | } 38 | 39 | @Override 40 | public SearchResultResponseDto searchInternshipAnnouncement(String keyword, String sortBy, Pageable pageable, Long userId) { 41 | Page pageAnnouncements = internshipRepository.searchInternshipAnnouncement(keyword, sortBy, pageable); 42 | 43 | List announcements = pageAnnouncements.getContent(); 44 | 45 | List searchAnnouncementResponses; 46 | 47 | List scraps = scrapRepository.findAllByInternshipAndUserId(announcements, userId); 48 | 49 | // 스크랩 정보를 매핑 (인턴 공고 ID -> 스크랩) 50 | Map scrapMap = scraps.stream() 51 | .collect(Collectors.toMap( 52 | scrap -> scrap.getInternshipAnnouncement().getId(), 53 | scrap -> scrap 54 | )); 55 | 56 | searchAnnouncementResponses = announcements.stream() 57 | .map(a -> { 58 | Scrap scrap = scrapMap.get(a.getId()); 59 | return SearchResultResponseDto.SearchAnnouncementResponse.from( 60 | a, scrap != null ? scrap.getId() : null, 61 | scrap != null ? scrap.getColor().getColorValue() : null 62 | ); 63 | }) 64 | .toList(); 65 | 66 | return new SearchResultResponseDto( 67 | pageAnnouncements.getTotalPages(), 68 | pageAnnouncements.getTotalElements(), 69 | pageAnnouncements.hasNext(), 70 | searchAnnouncementResponses 71 | ); 72 | } 73 | } -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/scrap/repository/ScrapRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.scrap.repository; 2 | 3 | import com.querydsl.jpa.impl.JPAQueryFactory; 4 | import lombok.RequiredArgsConstructor; 5 | import org.terning.terningserver.internshipAnnouncement.domain.InternshipAnnouncement; 6 | import org.terning.terningserver.scrap.domain.Scrap; 7 | 8 | import java.time.LocalDate; 9 | import java.util.List; 10 | 11 | import static org.terning.terningserver.scrap.domain.QScrap.scrap; 12 | 13 | @RequiredArgsConstructor 14 | public class ScrapRepositoryImpl implements ScrapRepositoryCustom{ 15 | 16 | private final JPAQueryFactory jpaQueryFactory; 17 | 18 | @Override 19 | public List findAllByInternshipAndUserId(List internshipAnnouncements, Long userId) { 20 | return jpaQueryFactory 21 | .selectFrom(scrap) 22 | .where(scrap.internshipAnnouncement.in(internshipAnnouncements), scrap.user.id.eq(userId)) 23 | .fetch(); 24 | } 25 | 26 | @Override 27 | public Long findScrapIdByInternshipAnnouncementIdAndUserId(Long internshipAnnouncementId, Long userId) { 28 | return jpaQueryFactory 29 | .select(scrap.id) 30 | .from(scrap) 31 | .where(scrap.internshipAnnouncement.id.eq(internshipAnnouncementId) 32 | .and(scrap.user.id.eq(userId))) 33 | .fetchOne(); 34 | } 35 | 36 | @Override 37 | public List findScrapsByUserIdAndDeadlineBetweenOrderByDeadline(Long userId, LocalDate start, LocalDate end){ 38 | return jpaQueryFactory 39 | .selectFrom(scrap) 40 | .where(scrap.user.id.eq(userId) 41 | .and(scrap.internshipAnnouncement.deadline.between(start, end))) 42 | .orderBy(scrap.internshipAnnouncement.deadline.asc()) 43 | .fetch(); 44 | } 45 | 46 | @Override 47 | public List findScrapsByUserIdAndDeadlineOrderByDeadline(Long userId, LocalDate deadline){ 48 | return jpaQueryFactory 49 | .selectFrom(scrap) 50 | .where(scrap.user.id.eq(userId) 51 | .and(scrap.internshipAnnouncement.deadline.eq(deadline))) 52 | .orderBy(scrap.internshipAnnouncement.deadline.asc()) 53 | .fetch(); 54 | } 55 | 56 | @Override 57 | public String findColorByInternshipAnnouncementIdAndUserId(Long internshipAnnouncementId, Long userId){ 58 | Scrap foundScrap = jpaQueryFactory 59 | .selectFrom(scrap) 60 | .where(scrap.internshipAnnouncement.id.eq(internshipAnnouncementId) 61 | .and(scrap.user.id.eq(userId))) 62 | .fetchOne(); 63 | 64 | return foundScrap != null ? foundScrap.getColorToHexValue() : null; 65 | } 66 | 67 | @Override 68 | public List findUserIdsWithUnsyncedScraps() { 69 | return jpaQueryFactory 70 | .select(scrap.user.id) 71 | .from(scrap) 72 | .where(scrap.syncStatus.value.eq(false)) 73 | .distinct() 74 | .fetch(); 75 | } 76 | 77 | @Override 78 | public List findUnsyncedScrapsByUserIds(List userIds) { 79 | return jpaQueryFactory 80 | .selectFrom(scrap) 81 | .where( 82 | scrap.user.id.in(userIds), 83 | scrap.syncStatus.value.eq(false) 84 | ) 85 | .fetch(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/application/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.application; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.stereotype.Service; 5 | import org.springframework.transaction.annotation.Transactional; 6 | import org.terning.terningserver.user.domain.User; 7 | import org.terning.terningserver.user.domain.ProfileImage; 8 | import org.terning.terningserver.user.domain.PushNotificationStatus; 9 | import org.terning.terningserver.user.dto.request.ProfileUpdateRequestDto; 10 | import org.terning.terningserver.common.exception.CustomException; 11 | import org.terning.terningserver.common.exception.enums.ErrorMessage; 12 | import org.terning.terningserver.external.pushNotification.notification.NotificationUserClient; 13 | import org.terning.terningserver.external.pushNotification.user.application.UserSyncEventService; 14 | import org.terning.terningserver.user.repository.UserRepository; 15 | import org.terning.terningserver.user.dto.response.ProfileResponseDto; 16 | 17 | import static org.terning.terningserver.common.exception.enums.ErrorMessage.FAILED_WITHDRAW; 18 | 19 | @Service 20 | @RequiredArgsConstructor 21 | @Transactional(readOnly = true) 22 | public class UserServiceImpl implements UserService { 23 | 24 | private final UserRepository userRepository; 25 | private final UserSyncEventService userSyncEventService; 26 | private final NotificationUserClient notificationUserClient; 27 | 28 | @Override 29 | @Transactional 30 | public void deleteUser(User user) { 31 | try { 32 | userRepository.delete(user); 33 | // userSyncEventService.recordWithdraw(user.getId()); 34 | notificationUserClient.deleteUser(user.getId()); 35 | } catch (Exception e) { 36 | throw new CustomException(FAILED_WITHDRAW); 37 | } 38 | } 39 | 40 | @Override 41 | public ProfileResponseDto getProfile(Long userId){ 42 | User user = userRepository.findById(userId).orElseThrow( 43 | () -> new CustomException(ErrorMessage.NOT_FOUND_USER_EXCEPTION) 44 | ); 45 | 46 | return ProfileResponseDto.of(user); 47 | } 48 | 49 | @Override 50 | @Transactional 51 | public void updateProfile(Long userId, ProfileUpdateRequestDto request){ 52 | User user = userRepository.findById(userId) 53 | .orElseThrow(() -> new CustomException(ErrorMessage.NOT_FOUND_USER_EXCEPTION)); 54 | 55 | try{ 56 | // 프로필 이미지가 유효하지 않으면 IllegalArgumentException을 던짐 57 | ProfileImage profileImage = ProfileImage.fromValue(request.profileImage()); 58 | 59 | if (!user.getName().equals(request.name())) { 60 | // userSyncEventService.recordNameChange(userId, request.name()); 61 | notificationUserClient.updateUserName(userId, request.name()); 62 | } 63 | 64 | //프로필 업데이트 65 | user.updateProfile(request.name(), ProfileImage.fromValue(request.profileImage())); 66 | 67 | userRepository.save(user); 68 | } catch (IllegalArgumentException e){ 69 | // 잘못된 프로필 이미지 값이 오면 CustomException 발생 70 | throw new CustomException(ErrorMessage.INVALID_PROFILE_IMAGE); 71 | } 72 | } 73 | 74 | @Transactional 75 | public void updatePushStatus(Long userId, String newStatus) { 76 | User user = userRepository.findById(userId) 77 | .orElseThrow(() -> new CustomException(ErrorMessage.NOT_FOUND_USER_EXCEPTION)); 78 | user.setPushStatus(PushNotificationStatus.from(newStatus)); 79 | userRepository.save(user); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/filter/application/FilterServiceImpl.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.filter.application; 2 | 3 | 4 | import lombok.RequiredArgsConstructor; 5 | import org.springframework.stereotype.Service; 6 | import org.springframework.transaction.annotation.Transactional; 7 | import org.terning.terningserver.filter.domain.Filter; 8 | import org.terning.terningserver.user.domain.User; 9 | import org.terning.terningserver.filter.domain.Grade; 10 | import org.terning.terningserver.filter.domain.JobType; 11 | import org.terning.terningserver.filter.domain.WorkingPeriod; 12 | import org.terning.terningserver.filter.dto.request.UpdateUserFilterRequestDto; 13 | import org.terning.terningserver.filter.dto.response.UserFilterDetailResponseDto; 14 | import org.terning.terningserver.common.exception.CustomException; 15 | import org.terning.terningserver.filter.repository.FilterRepository; 16 | import org.terning.terningserver.user.repository.UserRepository; 17 | 18 | import static org.terning.terningserver.common.exception.enums.ErrorMessage.NOT_FOUND_USER_EXCEPTION; 19 | 20 | @Service 21 | @RequiredArgsConstructor 22 | @Transactional(readOnly = true) 23 | public class FilterServiceImpl implements FilterService { 24 | 25 | private final UserRepository userRepository; 26 | private final FilterRepository filterRepository; 27 | 28 | @Override 29 | public UserFilterDetailResponseDto getUserFilter(Long userId) { 30 | return UserFilterDetailResponseDto.of(findUser(userId).getFilter()); 31 | } 32 | 33 | @Override 34 | @Transactional 35 | public void updateUserFilter(UpdateUserFilterRequestDto responseDto, Long userId) { 36 | User user = findUser(userId); 37 | Filter filter = user.getFilter(); 38 | 39 | JobType jobType = (responseDto.jobType() == null || responseDto.jobType().isBlank()) 40 | ? JobType.TOTAL : JobType.fromKey(responseDto.jobType()); 41 | Grade grade = (responseDto.grade() == null || responseDto.grade().isBlank()) 42 | ? null : Grade.fromKey(responseDto.grade()); 43 | WorkingPeriod workingPeriod = (responseDto.workingPeriod() == null || responseDto.workingPeriod().isBlank()) 44 | ? null : WorkingPeriod.fromKey(responseDto.workingPeriod()); 45 | 46 | if (filter != null) { 47 | updateExistingFilter(filter, jobType, grade, workingPeriod, responseDto); 48 | } 49 | 50 | createFilter(user, jobType, grade, workingPeriod, responseDto); 51 | } 52 | 53 | private User findUser(Long userId) { 54 | return userRepository.findById(userId) 55 | .orElseThrow(() -> new CustomException(NOT_FOUND_USER_EXCEPTION)); 56 | } 57 | 58 | private void updateExistingFilter(Filter filter, JobType jobType, Grade grade, WorkingPeriod workingPeriod, UpdateUserFilterRequestDto dto) { 59 | filter.updateFilter( 60 | jobType, 61 | grade, 62 | workingPeriod, 63 | dto.startYear(), 64 | dto.startMonth() 65 | ); 66 | } 67 | 68 | private void createFilter(User user, JobType jobType, Grade grade, WorkingPeriod workingPeriod, UpdateUserFilterRequestDto dto) { 69 | Filter savedFilter = filterRepository.save( 70 | Filter.builder() 71 | .jobType(jobType) 72 | .grade(grade) 73 | .workingPeriod(workingPeriod) 74 | .startYear(dto.startYear()) 75 | .startMonth(dto.startMonth()) 76 | .build() 77 | ); 78 | user.assignFilter(savedFilter); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/user/domain/User.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.user.domain; 2 | 3 | import jakarta.persistence.CascadeType; 4 | import jakarta.persistence.Column; 5 | import jakarta.persistence.Entity; 6 | import jakarta.persistence.Enumerated; 7 | import jakarta.persistence.FetchType; 8 | import jakarta.persistence.GeneratedValue; 9 | import jakarta.persistence.GenerationType; 10 | import jakarta.persistence.Id; 11 | import jakarta.persistence.JoinColumn; 12 | import jakarta.persistence.OneToMany; 13 | import jakarta.persistence.OneToOne; 14 | import jakarta.persistence.Table; 15 | import lombok.AccessLevel; 16 | import lombok.AllArgsConstructor; 17 | import lombok.Builder; 18 | import lombok.Getter; 19 | import lombok.NoArgsConstructor; 20 | import lombok.Setter; 21 | import org.terning.terningserver.auth.dto.request.SignUpRequest; 22 | import org.terning.terningserver.auth.jwt.exception.JwtErrorCode; 23 | import org.terning.terningserver.auth.jwt.exception.JwtException; 24 | import org.terning.terningserver.common.BaseTimeEntity; 25 | import org.terning.terningserver.common.exception.CustomException; 26 | import org.terning.terningserver.filter.domain.Filter; 27 | import org.terning.terningserver.scrap.domain.Scrap; 28 | 29 | import java.util.ArrayList; 30 | import java.util.List; 31 | 32 | import static jakarta.persistence.EnumType.STRING; 33 | import static org.terning.terningserver.common.exception.enums.ErrorMessage.FAILED_REFRESH_TOKEN_RESET; 34 | 35 | @Entity 36 | @Table(name = "Users") 37 | @Getter 38 | @Builder 39 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 40 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 41 | public class User extends BaseTimeEntity { 42 | 43 | @Id 44 | @GeneratedValue(strategy = GenerationType.IDENTITY) 45 | private Long id; 46 | 47 | @OneToOne(fetch = FetchType.LAZY) 48 | @JoinColumn(name="filter_id") 49 | private Filter filter; 50 | 51 | @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) 52 | private List scrapList = new ArrayList<>(); 53 | 54 | @Column(length = 12) 55 | private String name; 56 | 57 | @Column(length = 256) 58 | private String authId; 59 | 60 | @Column(length = 256) 61 | private String refreshToken; 62 | 63 | @Enumerated(STRING) 64 | private ProfileImage profileImage; 65 | 66 | @Enumerated(STRING) 67 | private AuthType authType; 68 | 69 | @Setter 70 | @Enumerated(STRING) 71 | private PushNotificationStatus pushStatus; 72 | 73 | @Enumerated(STRING) 74 | private State state; 75 | 76 | public static User from(String authId, SignUpRequest request) { 77 | return User.builder() 78 | .authId(authId) 79 | .name(request.name()) 80 | .authType(request.authType()) 81 | .profileImage(ProfileImage.fromValue(request.profileImage())) 82 | .pushStatus(PushNotificationStatus.ENABLED) 83 | .state(State.ACTIVE) 84 | .build(); 85 | } 86 | 87 | public void updateRefreshToken(String refreshToken) { 88 | this.refreshToken = refreshToken; 89 | } 90 | 91 | public void resetRefreshToken() { 92 | try { 93 | this.refreshToken = null; 94 | } catch (Exception e) { 95 | throw new CustomException(FAILED_REFRESH_TOKEN_RESET); 96 | } 97 | } 98 | 99 | public void assignFilter(Filter filter) { 100 | this.filter = filter; 101 | } 102 | 103 | public void updateProfile(String name, ProfileImage profileImage){ 104 | this.name = name; 105 | this.profileImage = profileImage; 106 | } 107 | 108 | public void validateRefreshToken(String providedToken) { 109 | if (this.refreshToken == null || !this.refreshToken.equals(providedToken)) { 110 | throw new JwtException(JwtErrorCode.INVALID_TOKEN); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/external/pushNotification/notification/NotificationUserClient.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.external.pushNotification.notification; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.http.HttpStatus; 6 | import org.springframework.web.reactive.function.client.WebClientResponseException; 7 | import org.terning.terningserver.user.domain.User; 8 | import org.terning.terningserver.user.domain.AuthType; 9 | import org.terning.terningserver.external.pushNotification.dto.request.CreateUserRequest; 10 | import org.springframework.stereotype.Component; 11 | import org.springframework.web.reactive.function.client.WebClient; 12 | import reactor.core.publisher.Mono; 13 | 14 | @Component 15 | @RequiredArgsConstructor 16 | @Slf4j 17 | public class NotificationUserClient { 18 | 19 | private final WebClient notificationWebClient; 20 | 21 | public void createUserOnNotificationServer( 22 | Long userId, 23 | String name, 24 | AuthType authType, 25 | String fcmToken 26 | ) { 27 | CreateUserRequest request = new CreateUserRequest( 28 | userId, 29 | name, 30 | authType, 31 | fcmToken, 32 | "enabled", 33 | "active" 34 | ); 35 | 36 | notificationWebClient.post() 37 | .uri("/api/v1/users") 38 | .body(Mono.just(request), CreateUserRequest.class) 39 | .retrieve() 40 | .bodyToMono(Void.class) 41 | .block(); 42 | 43 | log.info("User (id={}) created on notification server : ", userId); 44 | } 45 | 46 | public void createOrUpdateUser(User user, String fcmToken) { 47 | try { 48 | updateFcmToken(user.getId(), fcmToken); 49 | } catch (WebClientResponseException e) { 50 | if (e.getStatusCode() == HttpStatus.NOT_FOUND) { 51 | createUserOnNotificationServer( 52 | user.getId(), 53 | user.getName(), 54 | user.getAuthType(), 55 | fcmToken 56 | ); 57 | } else { 58 | throw e; 59 | } 60 | } 61 | } 62 | 63 | public void updateFcmToken(Long userId, String newToken) { 64 | notificationWebClient.put() 65 | .uri("/api/v1/users/{userId}/fcm-tokens", userId) 66 | .body(Mono.just(newToken), String.class) 67 | .retrieve() 68 | .bodyToMono(Void.class) 69 | .block(); 70 | 71 | log.info("FCM tokens updated for user (id={}): {}", userId, newToken); 72 | } 73 | 74 | public void updatePushStatus(Long userId, String newPushStatus) { 75 | notificationWebClient.put() 76 | .uri("/api/v1/users/{userId}/push-status", userId) 77 | .body(Mono.just(newPushStatus), String.class) 78 | .retrieve() 79 | .bodyToMono(Void.class) 80 | .block(); 81 | 82 | log.info("Push status updated for user (id={}): {}", userId, newPushStatus); 83 | } 84 | 85 | public void updateUserName(Long userId, String newName) { 86 | notificationWebClient.put() 87 | .uri("/api/v1/users/{userId}/name", userId) 88 | .body(Mono.just(newName), String.class) 89 | .retrieve() 90 | .bodyToMono(Void.class) 91 | .block(); 92 | 93 | log.info("User name updated for user (id={}): {}", userId, newName); 94 | } 95 | 96 | public void deleteUser(Long userId) { 97 | notificationWebClient.delete() 98 | .uri("/api/v1/users/{userId}", userId) 99 | .retrieve() 100 | .bodyToMono(Void.class) 101 | .block(); 102 | 103 | log.info("User (id={}) deleted on notification server.", userId); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/org/terning/terningserver/auth/api/AuthController.java: -------------------------------------------------------------------------------- 1 | package org.terning.terningserver.auth.api; 2 | 3 | import lombok.RequiredArgsConstructor; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.web.bind.annotation.*; 6 | import org.terning.terningserver.auth.application.AuthService; 7 | import org.terning.terningserver.auth.config.Login; 8 | import org.terning.terningserver.auth.dto.request.FcmTokenSyncRequest; 9 | import org.terning.terningserver.auth.dto.request.SignInRequest; 10 | import org.terning.terningserver.auth.dto.request.SignUpFilterRequest; 11 | import org.terning.terningserver.auth.dto.request.SignUpRequest; 12 | import org.terning.terningserver.auth.dto.response.SignInResponse; 13 | import org.terning.terningserver.auth.dto.response.SignUpResponse; 14 | import org.terning.terningserver.auth.dto.response.TokenReissueResponse; 15 | import org.terning.terningserver.common.exception.dto.SuccessResponse; 16 | 17 | import static org.terning.terningserver.auth.common.success.AuthSuccessCode.SUCCESS_SIGN_IN; 18 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_REISSUE_TOKEN; 19 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_SIGN_OUT; 20 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_SIGN_UP; 21 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_SIGN_UP_FILTER; 22 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_USER_SYNC; 23 | import static org.terning.terningserver.common.exception.enums.SuccessMessage.SUCCESS_WITHDRAW; 24 | 25 | 26 | @RestController 27 | @RequiredArgsConstructor 28 | @RequestMapping("/api/v1/auth") 29 | public class AuthController implements AuthSwagger { 30 | 31 | private final AuthService authService; 32 | 33 | @PostMapping("/sign-in") 34 | public ResponseEntity> signIn( 35 | @RequestHeader("Authorization") String socialAccessToken, 36 | @RequestBody SignInRequest request 37 | ) { 38 | SignInResponse response = authService.signIn(socialAccessToken, request); 39 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_SIGN_IN, response)); 40 | } 41 | 42 | @PostMapping("/token-reissue") 43 | public ResponseEntity> reissueToken( 44 | @RequestHeader("Authorization") String authorizationHeader 45 | ) { 46 | TokenReissueResponse response = authService.reissueAccessToken(authorizationHeader); 47 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_REISSUE_TOKEN, response)); 48 | } 49 | 50 | @PostMapping("/sign-up") 51 | public ResponseEntity> signUp( 52 | @RequestHeader("Authorization") String authId, 53 | @RequestBody SignUpRequest request 54 | ) { 55 | SignUpResponse signUpResponse = authService.signUp(authId, request); 56 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_SIGN_UP, signUpResponse)); 57 | } 58 | 59 | @PostMapping("/sign-up/filter") 60 | public ResponseEntity registerUserFilter( 61 | @RequestHeader("User-Id") Long userId, 62 | @RequestBody SignUpFilterRequest request 63 | ) { 64 | authService.registerUserFilter(userId, request); 65 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_SIGN_UP_FILTER)); 66 | } 67 | 68 | @PostMapping("/logout") 69 | public ResponseEntity signOut(@Login Long userId) { 70 | 71 | authService.signOut(userId); 72 | 73 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_SIGN_OUT)); 74 | } 75 | @DeleteMapping("/withdraw") 76 | public ResponseEntity withdraw(@Login Long userId) { 77 | 78 | authService.withdraw(userId); 79 | 80 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_WITHDRAW)); 81 | } 82 | 83 | @PostMapping("/sync-user") 84 | public ResponseEntity syncUser( 85 | @Login Long userId, 86 | @RequestBody FcmTokenSyncRequest request 87 | ) { 88 | authService.syncUser(userId, request); 89 | return ResponseEntity.ok(SuccessResponse.of(SUCCESS_USER_SYNC)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | bin/ 17 | !**/src/main/**/bin/ 18 | !**/src/test/**/bin/ 19 | 20 | ### IntelliJ IDEA ### 21 | .idea 22 | *.iws 23 | *.iml 24 | *.ipr 25 | out/ 26 | !**/src/main/**/out/ 27 | !**/src/test/**/out/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | ### Intellij template 40 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 41 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 42 | 43 | # User-specific stuff 44 | .idea/**/workspace.xml 45 | .idea/**/tasks.xml 46 | .idea/**/usage.statistics.xml 47 | .idea/**/dictionaries 48 | .idea/**/shelf 49 | 50 | # AWS User-specific 51 | .idea/**/aws.xml 52 | 53 | # Generated files 54 | .idea/**/contentModel.xml 55 | 56 | # Sensitive or high-churn files 57 | .idea/**/dataSources/ 58 | .idea/**/dataSources.ids 59 | .idea/**/dataSources.local.xml 60 | .idea/**/sqlDataSources.xml 61 | .idea/**/dynamic.xml 62 | .idea/**/uiDesigner.xml 63 | .idea/**/dbnavigator.xml 64 | 65 | # Gradle 66 | .idea/**/gradle.xml 67 | .idea/**/libraries 68 | 69 | # Gradle and Maven with auto-import 70 | # When using Gradle or Maven with auto-import, you should exclude module files, 71 | # since they will be recreated, and may cause churn. Uncomment if using 72 | # auto-import. 73 | # .idea/artifacts 74 | # .idea/compiler.xml 75 | # .idea/jarRepositories.xml 76 | # .idea/modules.xml 77 | # .idea/*.iml 78 | # .idea/modules 79 | # *.iml 80 | # *.ipr 81 | 82 | # CMake 83 | cmake-build-*/ 84 | 85 | # Mongo Explorer plugin 86 | .idea/**/mongoSettings.xml 87 | 88 | # File-based project format 89 | 90 | # IntelliJ 91 | 92 | # mpeltonen/sbt-idea plugin 93 | .idea_modules/ 94 | 95 | # JIRA plugin 96 | atlassian-ide-plugin.xml 97 | 98 | # Cursive Clojure plugin 99 | .idea/replstate.xml 100 | 101 | # SonarLint plugin 102 | .idea/sonarlint/ 103 | 104 | # Crashlytics plugin (for Android Studio and IntelliJ) 105 | com_crashlytics_export_strings.xml 106 | crashlytics.properties 107 | crashlytics-build.properties 108 | fabric.properties 109 | 110 | # Editor-based Rest Client 111 | .idea/httpRequests 112 | 113 | # Android studio 3.1+ serialized cache file 114 | .idea/caches/build_file_checksums.ser 115 | 116 | ### Java template 117 | # Compiled class file 118 | *.class 119 | 120 | # Log file 121 | *.log 122 | 123 | # BlueJ files 124 | *.ctxt 125 | 126 | # Mobile Tools for Java (J2ME) 127 | .mtj.tmp/ 128 | 129 | # Package Files # 130 | *.jar 131 | *.war 132 | *.nar 133 | *.ear 134 | *.zip 135 | *.tar.gz 136 | *.rar 137 | 138 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 139 | hs_err_pid* 140 | replay_pid* 141 | 142 | ### Gradle template 143 | **/build/ 144 | !src/**/build/ 145 | 146 | # Ignore Gradle GUI config 147 | gradle-app.setting 148 | 149 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 150 | !gradle-wrapper.jar 151 | 152 | # Avoid ignore Gradle wrappper properties 153 | !gradle-wrapper.properties 154 | 155 | # Cache of project 156 | .gradletasknamecache 157 | 158 | # Eclipse Gradle plugin generated files 159 | # Eclipse Core 160 | # JDT-specific (Eclipse Java Development Tools) 161 | 162 | ### macOS template 163 | # General 164 | .DS_Store 165 | .AppleDouble 166 | .LSOverride 167 | 168 | # Icon must end with two \r 169 | Icon 170 | 171 | # Thumbnails 172 | ._* 173 | 174 | # Files that might appear in the root of a volume 175 | .DocumentRevisions-V100 176 | .fseventsd 177 | .Spotlight-V100 178 | .TemporaryItems 179 | .Trashes 180 | .VolumeIcon.icns 181 | .com.apple.timemachine.donotpresent 182 | 183 | # Directories potentially created on remote AFP share 184 | .AppleDB 185 | .AppleDesktop 186 | Network Trash Folder 187 | Temporary Items 188 | .apdisk 189 | 190 | # application.yml 191 | src/main/resources/application.yml 192 | src/main/resources/application-dev.yml 193 | src/test/resources/application-test.yml 194 | 195 | # Q-Class 196 | src/main/generated 197 | --------------------------------------------------------------------------------