diff --git a/.github/workflows/database.yaml b/.github/workflows/database.yaml index 86818a91..9078ded5 100644 --- a/.github/workflows/database.yaml +++ b/.github/workflows/database.yaml @@ -33,6 +33,7 @@ jobs: with: host: ${{secrets.SSH_HOST}} username: ${{secrets.SSH_USER}} + port: ${{secrets.SSH_PORT}} key: ${{secrets.SSH_KEY}} source: "docker-compose-db.yml, .env" target: "~/app" @@ -44,6 +45,7 @@ jobs: with: host: ${{secrets.SSH_HOST}} username: ${{secrets.SSH_USER}} + port: ${{secrets.SSH_PORT}} key: ${{secrets.SSH_KEY}} script: | cd ~/app diff --git a/.github/workflows/database_dev.yaml b/.github/workflows/database_dev.yaml new file mode 100644 index 00000000..37873eeb --- /dev/null +++ b/.github/workflows/database_dev.yaml @@ -0,0 +1,52 @@ +on: + push: + branches: + - develop + paths: + - docker-compose-db.yml + - db_config/* + - .github/workflows/database.yaml + +jobs: + database-deploy: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + + steps: + - + name: Checkout + uses: actions/checkout@v3 + + - + name: Create .env file + run: | + echo "MYSQL_ROOT_PASSWORD=${{secrets.MYSQL_ROOT_PASSWORD}}" > .env + echo "MYSQL_USER=${{secrets.MYSQL_USER}}" >> .env + echo "MYSQL_PASSWORD=${{secrets.MYSQL_PASSWORD}}" >> .env + echo "MYSQL_DATABASE=${{secrets.MYSQL_DATABASE}}" >> .env + + - + name: SCP Command to Transfer Files + uses: appleboy/scp-action@v0.1.4 + with: + host: ${{secrets.SSH_HOST_DEV}} + username: ${{secrets.SSH_USER}} + key: ${{secrets.SSH_KEY}} + source: "docker-compose-db.yml, .env" + target: "~/app" + overwrite: true + + - + name: SSH Remote Commands + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{secrets.SSH_HOST_DEV}} + username: ${{secrets.SSH_USER}} + key: ${{secrets.SSH_KEY}} + script: | + cd ~/app + source .env + docker-compose -f docker-compose-db.yml down + docker-compose -f docker-compose-db.yml up -d diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 0c120312..8cc27deb 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -68,6 +68,7 @@ jobs: with: host: ${{secrets.SSH_HOST}} username: ${{secrets.SSH_USER}} + port: ${{secrets.SSH_PORT}} key: ${{secrets.SSH_KEY}} source: "docker-compose-backend.yml, .env" target: "~/app" @@ -77,8 +78,9 @@ jobs: with: host: ${{secrets.SSH_HOST}} username: ${{secrets.SSH_USER}} + port: ${{secrets.SSH_PORT}} key: ${{secrets.SSH_KEY}} - script: | # TODO: Change to blue-green deployment + script: | cd ~/app source .env docker-compose -f docker-compose-backend.yml down diff --git a/.github/workflows/deploy_dev.yaml b/.github/workflows/deploy_dev.yaml new file mode 100644 index 00000000..49bfb53b --- /dev/null +++ b/.github/workflows/deploy_dev.yaml @@ -0,0 +1,86 @@ +on: + push: + branches: + - develop + paths: + - docker-compose-backend.yml + - Dockerfile + - gradle.properties + - gradlew + - gradlew.bat + - build.gradle.kts + - settings.gradle.kts + - src/** + - gradle/** + - .github/workflows/deploy.yaml + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + permissions: + packages: write + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup Java JDK + uses: actions/setup-java@v3.12.0 + with: + java-version: '21' + distribution: 'temurin' + + - run: ./gradlew clean bootJar -x test + + - name: Log in to the Container Registry + uses: docker/login-action@v2.2.0 + with: + registry: ghcr.io + username: ${{github.actor}} + password: ${{secrets.GITHUB_TOKEN}} + + - name: Build and push Docker image + uses: docker/build-push-action@v4.1.1 + with: + context: . + push: true + build-args: | + PROFILE=prod + tags: | + ghcr.io/wafflestudio/csereal-server/server_image:latest + ghcr.io/wafflestudio/csereal-server/server_image:${{github.sha}} + + - name: Create .env file + run: | + echo "MYSQL_ROOT_PASSWORD=${{secrets.MYSQL_ROOT_PASSWORD}}" > .env + echo "MYSQL_USER=${{secrets.MYSQL_USER}}" >> .env + echo "MYSQL_PASSWORD=${{secrets.MYSQL_PASSWORD}}" >> .env + echo "MYSQL_DATABASE=${{secrets.MYSQL_DATABASE}}" >> .env + echo "PROFILE=dev" >> .env + echo "OIDC_CLIENT_SECRET=${{secrets.OIDC_CLIENT_SECRET}}" >> .env + echo "URL=${{secrets.URL_DEV}}" >> .env + # echo "SLACK_TOKEN=${{secrets.SLACK_TOKEN}}" >> .env + # echo "SLACK_CHANNEL=${{secrets.SLACK_CHANNEL}}" >> .env + + - name: SCP Command to Transfer Files + uses: appleboy/scp-action@v0.1.4 + with: + host: ${{secrets.SSH_HOST_DEV}} + username: ${{secrets.SSH_USER}} + key: ${{secrets.SSH_KEY}} + source: "docker-compose-backend.yml, .env" + target: "~/app" + overwrite: true + - name: SSH Remote Commands + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{secrets.SSH_HOST_DEV}} + username: ${{secrets.SSH_USER}} + key: ${{secrets.SSH_KEY}} + script: | + cd ~/app + source .env + docker-compose -f docker-compose-backend.yml down + docker-compose -f docker-compose-backend.yml pull + docker-compose -f docker-compose-backend.yml up -d diff --git a/.github/workflows/proxy.yaml b/.github/workflows/proxy.yaml index d158fa86..62fa771c 100644 --- a/.github/workflows/proxy.yaml +++ b/.github/workflows/proxy.yaml @@ -28,6 +28,7 @@ jobs: with: host: ${{secrets.SSH_HOST}} username: ${{secrets.SSH_USER}} + port: ${{secrets.SSH_PORT}} key: ${{secrets.SSH_KEY}} source: "docker-compose-caddy.yml, .env, caddy/Caddyfile" target: "~/proxy" @@ -39,6 +40,7 @@ jobs: with: host: ${{secrets.SSH_HOST}} username: ${{secrets.SSH_USER}} + port: ${{secrets.SSH_PORT}} key: ${{secrets.SSH_KEY}} script: | cd ~/proxy diff --git a/.github/workflows/proxy_dev.yaml b/.github/workflows/proxy_dev.yaml new file mode 100644 index 00000000..f1e8d553 --- /dev/null +++ b/.github/workflows/proxy_dev.yaml @@ -0,0 +1,47 @@ +on: + push: + branches: + - develop + paths: + - docker-compose-caddy.yml + - caddy/Caddyfile + - .github/workflows/proxy.yaml + +jobs: + proxy-initialize: + runs-on: ubuntu-latest + + steps: + - + name: Checkout + uses: actions/checkout@v3 + + - + name: Create .env file + run: | + echo "URL=${{secrets.URL_DEV}}" > .env + echo "LOCAL_IP=${{secrets.LOCAL_IP_DEV}}" >> .env + + - + name: SCP Command to Transfer Files + uses: appleboy/scp-action@v0.1.4 + with: + host: ${{secrets.SSH_HOST_DEV}} + username: ${{secrets.SSH_USER}} + key: ${{secrets.SSH_KEY}} + source: "docker-compose-caddy.yml, .env, caddy/Caddyfile" + target: "~/proxy" + overwrite: true + + - + name: SSH Command to Run Docker Compose + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{secrets.SSH_HOST_DEV}} + username: ${{secrets.SSH_USER}} + key: ${{secrets.SSH_KEY}} + script: | + cd ~/proxy + source .env + docker-compose -f docker-compose-caddy.yml down + docker-compose -f docker-compose-caddy.yml up -d diff --git a/build.gradle.kts b/build.gradle.kts index 758f74cf..e62c1599 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -63,9 +63,6 @@ dependencies { // 이미지 업로드 implementation("commons-io:commons-io:2.11.0") - // 썸네일 보여주기 - implementation("net.coobird:thumbnailator:0.4.19") - // Custom Metadata annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") } diff --git a/caddy/Caddyfile b/caddy/Caddyfile index 0eeefa95..11f81c5a 100644 --- a/caddy/Caddyfile +++ b/caddy/Caddyfile @@ -9,15 +9,15 @@ abort @backend_denied # Backend - reverse_proxy /api/* host.docker.internal:8080 #host.docker.internal:8081 # For blue/green + reverse_proxy /api/* host.docker.internal:8080 # Old file serving - reverse_proxy /sites/default/files/* host.docker.internal:8080 #host.docker.internal:8081 # For blue/green + reverse_proxy /sites/default/files/* host.docker.internal:8080 # Login - reverse_proxy /oauth2/authorization/idsnucse host.docker.internal:8080 #host.docker.internal:8081 # For blue/green + reverse_proxy /oauth2/authorization/idsnucse host.docker.internal:8080 # Swagger - reverse_proxy /swagger-ui/* host.docker.internal:8080 #host.docker.internal:8081 # For blue/green - reverse_proxy /api-docs/* host.docker.internal:8080 #host.docker.internal:8081 # For blue/green -} \ No newline at end of file + reverse_proxy /swagger-ui/* host.docker.internal:8080 + reverse_proxy /api-docs/* host.docker.internal:8080 +} diff --git a/docker-compose-backend.yml b/docker-compose-backend.yml index 82e3578a..ffba533b 100644 --- a/docker-compose-backend.yml +++ b/docker-compose-backend.yml @@ -23,25 +23,3 @@ services: - host.docker.internal:host-gateway restart: always image: ghcr.io/wafflestudio/csereal-server/server_image:latest - # TODO: Activate after implementing health check -# blue: -# container_name: csereal_server_blue -# build: -# context: ./ -# args: -# PROFILE: ${PROFILE} -# ports: -# - 8081:8080 -# volumes: -# - ./cse-files:/app/cse-files -# - ./attachment:/app/attachment -# environment: -# SPRING_DATASOURCE_URL: "jdbc:mysql://host.docker.internal:3306/${MYSQL_DATABASE}?serverTimezone=Asia/Seoul&useSSL=false&allowPublicKeyRetrieval=true" -# SPRING_DATASOURCE_USERNAME: ${MYSQL_USER} -# SPRING_DATASOURCE_PASSWORD: ${MYSQL_PASSWORD} -# OIDC_CLIENT_SECRET_DEV: ${OIDC_CLIENT_SECRET_DEV} -# URL: ${URL} -# extra_hosts: -# - host.docker.internal:host-gateway -# restart: always -# image: ghcr.io/wafflestudio/csereal-server/server_image:latest diff --git a/src/main/kotlin/com/wafflestudio/csereal/common/utils/Utils.kt b/src/main/kotlin/com/wafflestudio/csereal/common/utils/Utils.kt index da3564b8..d6daee44 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/common/utils/Utils.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/common/utils/Utils.kt @@ -56,3 +56,7 @@ fun getUsername(authentication: Authentication?): String? { } } } + +fun startsWithEnglish(name: String): Boolean { + return name.isNotEmpty() && name.first().let { it in 'A'..'Z' || it in 'a'..'z' } +} diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt index 4a334658..3f72d722 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/database/AboutEntity.kt @@ -28,7 +28,7 @@ class AboutEntity( @Convert(converter = StringListConverter::class) var locations: MutableList = mutableListOf(), - @OneToMany(mappedBy = "") + @OneToMany(mappedBy = "about", cascade = [CascadeType.ALL], orphanRemoval = true) var attachments: MutableList = mutableListOf(), @OneToOne diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt index 884e1554..faf6fdec 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/about/service/AboutService.kt @@ -6,10 +6,13 @@ import com.wafflestudio.csereal.core.about.api.res.AboutSearchElementDto import com.wafflestudio.csereal.core.about.api.res.AboutSearchResBody import com.wafflestudio.csereal.core.about.database.* import com.wafflestudio.csereal.core.about.dto.* +import com.wafflestudio.csereal.core.main.event.RefreshSearchEvent import com.wafflestudio.csereal.core.resource.attachment.service.AttachmentService import com.wafflestudio.csereal.core.resource.mainImage.service.MainImageService +import org.springframework.context.event.EventListener import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional import org.springframework.web.multipart.MultipartFile @@ -185,6 +188,14 @@ class AboutServiceImpl( return FutureCareersPage(description, statList, companyList) } + @Transactional(propagation = Propagation.REQUIRES_NEW) + @EventListener + fun refreshSearchListener(event: RefreshSearchEvent) { + aboutRepository.findAll().forEach { + syncSearchOfAbout(it) + } + } + @Transactional fun syncSearchOfAbout(about: AboutEntity) { if (about.postType == AboutPostType.FUTURE_CAREERS) { diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsSearchService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsSearchService.kt index 63a0b3dc..3a126a08 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsSearchService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/academics/service/AcademicsSearchService.kt @@ -1,9 +1,12 @@ package com.wafflestudio.csereal.core.academics.service import com.wafflestudio.csereal.common.properties.LanguageType -import com.wafflestudio.csereal.core.academics.database.AcademicsSearchRepository import com.wafflestudio.csereal.core.academics.api.res.AcademicsSearchResBody +import com.wafflestudio.csereal.core.academics.database.* +import com.wafflestudio.csereal.core.main.event.RefreshSearchEvent +import org.springframework.context.event.EventListener import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional interface AcademicsSearchService { @@ -20,11 +23,18 @@ interface AcademicsSearchService { pageNum: Int, amount: Int ): AcademicsSearchResBody + + fun syncCourseSearch(course: CourseEntity) + fun syncScholarshipSearch(scholarship: ScholarshipEntity) + fun syncAcademicsSearch(academics: AcademicsEntity) } @Service class AcademicsSearchServiceImpl( - private val academicsSearchRepository: AcademicsSearchRepository + private val academicsSearchRepository: AcademicsSearchRepository, + private val academicsRepository: AcademicsRepository, + private val courseRepository: CourseRepository, + private val scholarshipRepository: ScholarshipRepository ) : AcademicsSearchService { @Transactional(readOnly = true) override fun searchTopAcademics( @@ -68,4 +78,44 @@ class AcademicsSearchServiceImpl( amount = amount ) } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @EventListener + fun refreshSearchListener(event: RefreshSearchEvent) { + academicsRepository.findAll().forEach { + syncAcademicsSearch(it) + } + + courseRepository.findAll().forEach { + syncCourseSearch(it) + } + + scholarshipRepository.findAll().forEach { + syncScholarshipSearch(it) + } + } + + @Transactional + override fun syncAcademicsSearch(academics: AcademicsEntity) { + academics.academicsSearch?.update(academics) + ?: let { + academics.academicsSearch = AcademicsSearchEntity.create(academics) + } + } + + @Transactional + override fun syncScholarshipSearch(scholarship: ScholarshipEntity) { + scholarship.academicsSearch?.update(scholarship) + ?: let { + scholarship.academicsSearch = AcademicsSearchEntity.create(scholarship) + } + } + + @Transactional + override fun syncCourseSearch(course: CourseEntity) { + course.academicsSearch?.update(course) + ?: let { + course.academicsSearch = AcademicsSearchEntity.create(course) + } + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt index 2959dab1..2b9df4ce 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/admissions/service/AdmissionsService.kt @@ -11,7 +11,10 @@ import com.wafflestudio.csereal.core.admissions.database.AdmissionsRepository import com.wafflestudio.csereal.core.admissions.dto.AdmissionsDto import com.wafflestudio.csereal.core.admissions.type.AdmissionsMainType import com.wafflestudio.csereal.core.admissions.type.AdmissionsPostType +import com.wafflestudio.csereal.core.main.event.RefreshSearchEvent +import org.springframework.context.event.EventListener import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional interface AdmissionsService { @@ -83,6 +86,27 @@ class AdmissionsServiceImpl( ) } + @Transactional(propagation = Propagation.REQUIRES_NEW) + @EventListener + fun refreshSearch(event: RefreshSearchEvent) { + admissionsRepository.findAll().forEach { + syncSearchAdmission(it) + } + } + + @Transactional + fun syncSearchAdmission(admissions: AdmissionsEntity) { + admissions.apply { + searchContent = AdmissionsEntity.createSearchContent( + name, + mainType, + postType, + language, + description + ) + } + } + @Transactional(readOnly = true) override fun searchPageAdmission( keyword: String, diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/main/api/MainController.kt b/src/main/kotlin/com/wafflestudio/csereal/core/main/api/MainController.kt index fd85e79b..33a533d2 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/main/api/MainController.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/main/api/MainController.kt @@ -15,4 +15,9 @@ class MainController( fun readMain(): MainResponse { return mainService.readMain() } + + @GetMapping("/search/refresh") + fun refreshSearches() { + mainService.refreshSearch() + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/main/event/RefreshSearchEvent.kt b/src/main/kotlin/com/wafflestudio/csereal/core/main/event/RefreshSearchEvent.kt new file mode 100644 index 00000000..5bfadd5a --- /dev/null +++ b/src/main/kotlin/com/wafflestudio/csereal/core/main/event/RefreshSearchEvent.kt @@ -0,0 +1,3 @@ +package com.wafflestudio.csereal.core.main.event + +class RefreshSearchEvent diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/main/service/MainService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/main/service/MainService.kt index 1bef3b7e..fecebe6e 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/main/service/MainService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/main/service/MainService.kt @@ -3,17 +3,21 @@ package com.wafflestudio.csereal.core.main.service import com.wafflestudio.csereal.core.main.database.MainRepository import com.wafflestudio.csereal.core.main.dto.MainResponse import com.wafflestudio.csereal.core.main.dto.NoticesResponse +import com.wafflestudio.csereal.core.main.event.RefreshSearchEvent import com.wafflestudio.csereal.core.notice.database.TagInNoticeEnum +import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional interface MainService { fun readMain(): MainResponse + fun refreshSearch() } @Service class MainServiceImpl( - private val mainRepository: MainRepository + private val mainRepository: MainRepository, + private val eventPublisher: ApplicationEventPublisher ) : MainService { @Transactional(readOnly = true) override fun readMain(): MainResponse { @@ -29,4 +33,8 @@ class MainServiceImpl( return MainResponse(slides, notices, importants) } + + override fun refreshSearch() { + eventPublisher.publishEvent(RefreshSearchEvent()) + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/service/MemberSearchService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/MemberSearchService.kt index ca338b53..d6ba232e 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/service/MemberSearchService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/MemberSearchService.kt @@ -1,10 +1,16 @@ package com.wafflestudio.csereal.core.member.service import com.wafflestudio.csereal.common.properties.LanguageType +import com.wafflestudio.csereal.core.main.event.RefreshSearchEvent import com.wafflestudio.csereal.core.member.api.res.MemberSearchResBody +import com.wafflestudio.csereal.core.member.database.MemberSearchEntity import com.wafflestudio.csereal.core.member.database.MemberSearchRepository +import com.wafflestudio.csereal.core.member.database.ProfessorRepository +import com.wafflestudio.csereal.core.member.database.StaffRepository import com.wafflestudio.csereal.core.resource.mainImage.service.MainImageService +import org.springframework.context.event.EventListener import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional interface MemberSearchService { @@ -16,7 +22,9 @@ interface MemberSearchService { @Service class MemberSearchServiceImpl( private val memberSearchRepository: MemberSearchRepository, - private val mainImageService: MainImageService + private val mainImageService: MainImageService, + private val professorRepository: ProfessorRepository, + private val staffRepository: StaffRepository ) : MemberSearchService { @Transactional(readOnly = true) override fun searchTopMember(keyword: String, language: LanguageType, number: Int): MemberSearchResBody { @@ -34,4 +42,20 @@ class MemberSearchServiceImpl( val (entityResults, total) = memberSearchRepository.searchMember(keyword, language, pageSize, pageNum) return MemberSearchResBody.of(entityResults, total, mainImageService::createImageURL) } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @EventListener + fun refreshSearchListener(event: RefreshSearchEvent) { + professorRepository.findAll().forEach { pf -> + pf.memberSearch?.update(pf) ?: let { + pf.memberSearch = MemberSearchEntity.create(pf) + } + } + + staffRepository.findAll().forEach { st -> + st.memberSearch?.update(st) ?: let { + st.memberSearch = MemberSearchEntity.create(st) + } + } + } } diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorService.kt index d064b7f8..d62cf464 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/ProfessorService.kt @@ -2,6 +2,7 @@ package com.wafflestudio.csereal.core.member.service import com.wafflestudio.csereal.common.CserealException import com.wafflestudio.csereal.common.properties.LanguageType +import com.wafflestudio.csereal.common.utils.startsWithEnglish import com.wafflestudio.csereal.core.member.database.* import com.wafflestudio.csereal.core.member.dto.ProfessorDto import com.wafflestudio.csereal.core.member.dto.ProfessorPageDto @@ -27,6 +28,7 @@ interface ProfessorService { updateProfessorRequest: ProfessorDto, mainImage: MultipartFile? ): ProfessorDto + fun deleteProfessor(professorId: Long) fun migrateProfessors(requestList: List): List fun migrateProfessorImage(professorId: Long, mainImage: MultipartFile): ProfessorDto @@ -94,6 +96,8 @@ class ProfessorServiceImpl( @Transactional(readOnly = true) override fun getActiveProfessors(language: String): ProfessorPageDto { val enumLanguageType = LanguageType.makeStringToLanguageType(language) + + // TODO: Refactor to save in database val description = "컴퓨터공학부는 35명의 훌륭한 교수진과 최신 시설을 갖추고 400여 명의 학부생과 " + "350여 명의 대학원생에게 세계 최고 수준의 교육 연구 환경을 제공하고 있다. 2005년에는 서울대학교 " + "최초로 외국인 정교수인 Robert Ian McKay 교수를 임용한 것을 시작으로 교내에서 가장 국제화가 " + @@ -102,6 +106,7 @@ class ProfessorServiceImpl( " 학기 전공 필수 과목을 비롯한 30% 이상의 과목이 영어로 개설되고 있어 외국인 학생의 학업을 돕는 " + "동시에 한국인 학생이 세계로 진출하는 초석이 되고 있다. 또한 CSE int’l Luncheon을 개최하여 " + "학부 내 외국인 구성원의 화합과 생활의 불편함을 최소화하는 등 학부 차원에서 최선을 다하고 있다." + val professors = professorRepository.findByLanguageAndStatusNot( enumLanguageType, @@ -110,7 +115,14 @@ class ProfessorServiceImpl( val imageURL = mainImageService.createImageURL(it.mainImage) SimpleProfessorDto.of(it, imageURL) } - .sortedBy { it.name } + .sortedWith { a, b -> + when { + startsWithEnglish(a.name) && !startsWithEnglish(b.name) -> 1 + !startsWithEnglish(a.name) && !startsWithEnglish(b.name) -> -1 + else -> a.name.compareTo(b.name) + } + } + return ProfessorPageDto(description, professors) } @@ -124,7 +136,13 @@ class ProfessorServiceImpl( val imageURL = mainImageService.createImageURL(it.mainImage) SimpleProfessorDto.of(it, imageURL) } - .sortedBy { it.name } + .sortedWith { a, b -> + when { + startsWithEnglish(a.name) && !startsWithEnglish(b.name) -> 1 + !startsWithEnglish(a.name) && !startsWithEnglish(b.name) -> -1 + else -> a.name.compareTo(b.name) + } + } } override fun updateProfessor( diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt index 32d5f9a4..09fabba5 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/member/service/StaffService.kt @@ -65,10 +65,17 @@ class StaffServiceImpl( override fun getAllStaff(language: String): List { val enumLanguageType = LanguageType.makeStringToLanguageType(language) - return staffRepository.findAllByLanguage(enumLanguageType).map { + val sortedStaff = staffRepository.findAllByLanguage(enumLanguageType).map { val imageURL = mainImageService.createImageURL(it.mainImage) SimpleStaffDto.of(it, imageURL) - }.sortedBy { it.name } + }.sortedBy { it.name }.toMutableList() + + sortedStaff.indexOfFirst { it.email == "misuk@snu.ac.kr" }.takeIf { it != -1 }?.let { index -> + val headStaff = sortedStaff.removeAt(index) + sortedStaff.add(0, headStaff) + } + + return sortedStaff } override fun updateStaff(staffId: Long, updateStaffRequest: StaffDto, mainImage: MultipartFile?): StaffDto { diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchSearchService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchSearchService.kt index 8b9b0bac..90f0f2b6 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchSearchService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/research/service/ResearchSearchService.kt @@ -1,6 +1,8 @@ package com.wafflestudio.csereal.core.research.service import com.wafflestudio.csereal.common.properties.LanguageType +import com.wafflestudio.csereal.core.conference.database.ConferenceRepository +import com.wafflestudio.csereal.core.main.event.RefreshSearchEvent import com.wafflestudio.csereal.core.member.event.ProfessorCreatedEvent import com.wafflestudio.csereal.core.member.event.ProfessorDeletedEvent import com.wafflestudio.csereal.core.member.event.ProfessorModifiedEvent @@ -8,9 +10,11 @@ import com.wafflestudio.csereal.core.research.database.LabRepository import com.wafflestudio.csereal.core.research.database.ResearchSearchEntity import com.wafflestudio.csereal.core.research.database.ResearchSearchRepository import com.wafflestudio.csereal.core.research.api.res.ResearchSearchResBody +import com.wafflestudio.csereal.core.research.database.ResearchRepository import org.springframework.context.event.EventListener import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Propagation import org.springframework.transaction.annotation.Transactional interface ResearchSearchService { @@ -31,7 +35,9 @@ interface ResearchSearchService { @Service class ResearchSearchServiceImpl( private val labRepository: LabRepository, - private val researchSearchRepository: ResearchSearchRepository + private val researchSearchRepository: ResearchSearchRepository, + private val researchRepository: ResearchRepository, + private val conferenceRepository: ConferenceRepository ) : ResearchSearchService { @Transactional(readOnly = true) override fun searchTopResearch( @@ -118,6 +124,28 @@ class ResearchSearchServiceImpl( } } + @EventListener + @Transactional(propagation = Propagation.REQUIRES_NEW) + fun refreshSearchListener(event: RefreshSearchEvent) { + labRepository.findAll().forEach { + it.researchSearch?.update(it) ?: let { _ -> + it.researchSearch = ResearchSearchEntity.create(it) + } + } + + researchRepository.findAll().forEach { + it.researchSearch?.update(it) ?: let { _ -> + it.researchSearch = ResearchSearchEntity.create(it) + } + } + + conferenceRepository.findAll().forEach { + it.researchSearch?.update(it) ?: let { _ -> + it.researchSearch = ResearchSearchEntity.create(it) + } + } + } + @Transactional override fun deleteResearchSearch( researchSearchEntity: ResearchSearchEntity diff --git a/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt index c62058e4..614ced84 100644 --- a/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt +++ b/src/main/kotlin/com/wafflestudio/csereal/core/resource/mainImage/service/MainImageService.kt @@ -12,7 +12,6 @@ import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageReposi import com.wafflestudio.csereal.core.resource.mainImage.database.MainImageEntity import com.wafflestudio.csereal.core.resource.mainImage.dto.MainImageDto import com.wafflestudio.csereal.core.seminar.database.SeminarEntity -import net.coobird.thumbnailator.Thumbnailator import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -21,8 +20,6 @@ import org.apache.commons.io.FilenameUtils import java.lang.invoke.WrongMethodTypeException import java.nio.file.Files import java.nio.file.Paths -import kotlin.io.path.fileSize -import kotlin.io.path.name interface MainImageService { fun uploadMainImage( @@ -61,25 +58,14 @@ class MainImageServiceImpl( val saveFile = Paths.get(totalFilename) requestImage.transferTo(saveFile) - val totalThumbnailFilename = "${path}thumbnail_$filename" - val thumbnailFile = Paths.get(totalThumbnailFilename) - Thumbnailator.createThumbnail(saveFile.toFile(), thumbnailFile.toFile(), 100, 100) - val mainImage = MainImageEntity( filename = filename, imagesOrder = 1, size = requestImage.size ) - val thumbnail = MainImageEntity( - filename = thumbnailFile.name, - imagesOrder = 1, - size = thumbnailFile.fileSize() - ) - connectMainImageToEntity(contentEntityType, mainImage) mainImageRepository.save(mainImage) - mainImageRepository.save(thumbnail) return MainImageDto( filename = filename, diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index df47478b..04f23b8d 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -6,6 +6,10 @@ spring: active: local datasource: driver-class-name: com.mysql.cj.jdbc.Driver + jpa: + properties: + hibernate: + dialect: com.wafflestudio.csereal.common.config.MySQLDialectCustom servlet: multipart: enabled: true @@ -50,9 +54,6 @@ spring: ddl-auto: update show-sql: true open-in-view: false - properties: - hibernate: - dialect: com.wafflestudio.csereal.common.config.MySQLDialectCustom security: oauth2: client: @@ -80,11 +81,8 @@ spring: config.activate.on-profile: prod jpa: hibernate: - ddl-auto: update # TODO: change to validate (or none) when save actual data to server + ddl-auto: validate open-in-view: false - properties: - hibernate: - dialect: com.wafflestudio.csereal.common.config.MySQLDialectCustom security: oauth2: client: @@ -117,6 +115,42 @@ slack: token: ${SLACK_TOKEN} channel: ${SLACK_CHANNEL} +--- +spring: + config.activate.on-profile: dev + jpa: + hibernate: + ddl-auto: update + show-sql: true + open-in-view: false + security: + oauth2: + client: + registration: + idsnucse: + client-id: cse-waffle-dev + client-secret: ${OIDC_CLIENT_SECRET} + authorization-grant-type: authorization_code + scope: openid, profile, email + redirect-uri: https://${URL}/api/v1/login/oauth2/code/idsnucse + provider: + idsnucse: + issuer-uri: https://id.snucse.org/o + jwk-set-uri: https://id.snucse.org/o/jwks + +csereal: + upload: + path: /app/files/ + +oldFiles: + path: /app/cse-files/ + +endpoint: + backend: https://${URL}/api + frontend: https://${URL} + +login-page: https://${URL} + --- spring: config.activate.on-profile: test diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index 867810e6..d7d3352b 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -13,7 +13,7 @@ - + @@ -42,10 +42,17 @@ + + + + + + +