diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
new file mode 100644
index 00000000..49eb7679
--- /dev/null
+++ b/.github/workflows/deploy.yml
@@ -0,0 +1,47 @@
+name: ssh for release deploy
+
+on:
+ push:
+ branches: [ develop ]
+
+jobs:
+ build:
+ name: Build
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: SSH Remote Commands
+ uses: appleboy/ssh-action@v0.1.4
+ with:
+ host: ${{ secrets.HOST }}
+ username: ${{ secrets.USERNAME }}
+ key: ${{ secrets.KEY }}
+ port: ${{ secrets.PORT }}
+ timeout: 40s
+
+ script: |
+ echo "1. project root로 이동"
+ cd ~/livable/server
+
+ echo "2. git pull"
+ git pull origin develop
+
+ echo "3. project build"
+ ./gradlew clean build
+
+ if [ $? -eq 0 ]; then
+ # 빌드 성공 시
+ echo "4. 실행중인 프로세스 확인"
+ CURRENT_PID=$(pgrep -f ${{ secrets.PROJECT_ROOT }}/build/libs/server-0.0.1-SNAPSHOT.jar)
+
+ echo "5. 실행중인 프로세스 중지"
+ sudo kill -9 $CURRENT_PID
+
+ echo "6. 홈 경로 이동 후 프로젝트 재실행"
+ cd ~
+ nohup java -jar ${{ secrets.PROJECT_ROOT }}/build/libs/server-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > nohup.out 2> nohup.err < /dev/null &
+ else
+ echo "프로젝트 Build 실패"
+ exit 1
+ fi
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 00000000..89fc3b1e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,43 @@
+HELP.md
+.gradle
+build/
+!gradle/wrapper/gradle-wrapper.jar
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### jacoco ###
+jacoco/
+
+### STS ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+bin/
+!**/src/main/**/bin/
+!**/src/test/**/bin/
+
+### IntelliJ IDEA ###
+.idea
+*.iws
+*.iml
+*.ipr
+out/
+!**/src/main/**/out/
+!**/src/test/**/out/
+application-dev.yml
+application-prod.yml
+/src/test/resources/test.properties
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+
+### VS Code ###
+.vscode/
diff --git a/README.md b/README.md
index 59069fb5..1532fb24 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,182 @@
-# server
-오피스 혁신을 위한 통합 플랫폼 ‘오피스너’의 유저 가입자수와 WAU를 높이기 위한 프로젝트
+
+
+
+
🏢 오피스 혁신을 위한 통합 플랫폼, 오피스너
+
+> **개발 기간** : 2023.09.11(월) ~ 2023.10.06(목)
+> **배포 주소** : [오피스너](https://officener.vercel.app)
+> **백엔드 레포지토리** : [백엔드](https://github.com/livable-final/server)
+> **프론트 유저 레포지토리** : [프론트](https://github.com/livable-final/client)
+
+
+
+
+프로젝트 목적
+
+- 기존 오피스너 서비스는 관리자, 관리 멤버 이외의 일반 유저의 가입과 이용 동기가 부족
+- 이용자가 매일 사용해야 할 만한 컨텐츠와 기능의 부재
+- "유저 가입자 수"와 "WAU" 상승을 목적으로 시작된 기업 연계 프로젝트
+
+
+
+사용한 기술스택
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+백엔드 아키텍처
+
+
+
+
+
+
+
+ERD
+
+
+
+
+
+
+
+Jacoco 테스트 커버리지
+
+> 백엔드팀 테스트 커버리지 목표 40% 이상 달성
+
+
+
+
+
+
+
+
+프로젝트 실행하기
+
+### application.yml
+``` yaml
+# Spring, DB propertiesg setting
+spring:
+ datasource:
+ url: #DB Address
+ driver-class-name: #DB Driver
+ username: #DB Username
+ password: #DB Password
+
+ jpa:
+ hibernate:
+ ddl-auto: update
+ properties:
+ hibernate:
+ default_batch_fetch_size: 100
+
+ quartz:
+ auto-startup: true
+ job-store-type: jdbc
+ jdbc:
+ initialize-schema: never
+ overwrite-existing-jobs: false
+
+# S3 propertiesg setting
+cloud:
+ aws:
+ s3:
+ bucket: #S3 bucketName
+ credentials:
+ accessKey: #S3 accessKey
+ secretKey: #S3 secretKey
+ region:
+ static: ap-northeast-2
+ stack:
+ auto: false
+
+# JWT properties setting
+jwt:
+ secret: #JWT key
+
+#
+```
+
+### build and test
+```bash
+$ ./gradlew clean build
+```
+
+### run
+```bash
+$ java -jar ./build/libs/server-0.0.1-SNAPSHOT.jar
+```
+
+
+
+
+
+백엔드 팀원
+
+
+
+
+팀원 역할
+
+
+
+ | 이름 | 역할 |
+ | :-----------------: | -------------------------------- |
+ | 정현수
`팀장` | - 데이터베이스 설계
- API 명세서 설계
- S3 업로드 환경 구성
- 포인트 API 구현
- 식당 리뷰 API 구현
- 다중 이미지 처리 구현
|
+ | 김훈섭 | - 데이터베이스 설계
- API 명세서 설계
- EC2 서버환경 구성
- 자동화 배포환경 구성
- SSL 적용 (HTTPS)
- 초대장 CRUD 구현
- Kakao 알림톡 적용
|
+ | 최태윤 | - 데이터베이스 설계
- API 명세서 설계
- 자동화 배포환경 구성
- 방문증 API 구현
|
+ | 김태일 | - 데이터 베이스 설계
- API 명세서 설계
- 식당 리뷰 API 구현
|
+ | 배종윤 | - 데이터 베이스 설계
- API 명세서 설계
- 홈 화면 API 구현
- 오늘 점심 뭐먹지 룰렛 API 구현
- Spring Quartz 스케쥴러를 이용한 집계 기능 구현
|
+
+
+
+
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..4af6188e
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,155 @@
+buildscript {
+ ext {
+ queryDslVersion = "5.0.0"
+ }
+}
+
+plugins {
+ id 'java'
+ id 'jacoco' //jacoco
+ id 'org.springframework.boot' version '2.7.15'
+ id 'io.spring.dependency-management' version '1.0.15.RELEASE'
+ id 'org.asciidoctor.jvm.convert' version '3.3.2'
+ id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
+}
+
+jacoco {
+ toolVersion = "0.8.10"
+}
+
+/*jacoco setting start*/
+jacocoTestReport {
+ dependsOn test
+ reports {
+ html.required = true
+ xml.required = true
+ }
+
+ def Qdomains = []
+ for (qPattern in "**/QA".."**/QZ") {
+ Qdomains.add(qPattern + "*")
+ }
+
+ afterEvaluate {
+ classDirectories.setFrom(files(classDirectories.files.collect {
+ fileTree(dir: it,
+ exclude: [] + Qdomains
+ )
+ }))
+ }
+ finalizedBy jacocoTestCoverageVerification
+}
+
+jacocoTestCoverageVerification {
+ def Qdomains = []
+ for (qPattern in "*.QA".."*.QZ") {
+ Qdomains.add(qPattern + "*")
+ }
+ violationRules {
+ rule {
+ limit {
+ minimum = 0.30
+ }
+ }
+
+ rule {
+ enabled = true
+
+
+ limit {
+ counter = 'METHOD'
+ value = 'COVEREDRATIO'
+ minimum = 0.50
+ }
+
+ excludes = [] + Qdomains
+ }
+ }
+}
+/*jacoco setting end*/
+
+group = 'com.livable'
+version = '0.0.1-SNAPSHOT'
+
+java {
+ sourceCompatibility = '11'
+}
+
+configurations {
+ compileOnly {
+ extendsFrom annotationProcessor
+ }
+}
+
+repositories {
+ mavenCentral()
+}
+
+ext {
+ set('snippetsDir', file("build/generated-snippets"))
+}
+
+dependencies {
+ implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
+ implementation 'org.springframework.boot:spring-boot-starter-validation'
+ implementation 'org.springframework.boot:spring-boot-starter-web'
+ implementation "com.querydsl:querydsl-jpa:${queryDslVersion}"
+ compileOnly 'org.projectlombok:lombok'
+ runtimeOnly 'com.h2database:h2'
+ runtimeOnly 'com.mysql:mysql-connector-j'
+ annotationProcessor 'org.projectlombok:lombok'
+ annotationProcessor "com.querydsl:querydsl-apt:${queryDslVersion}"
+ testImplementation 'org.springframework.boot:spring-boot-starter-test'
+ testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
+ testImplementation 'org.mockito:mockito-inline:3.6.0'
+ implementation 'com.google.zxing:core:3.5.2'
+ implementation 'com.google.zxing:javase:3.5.2'
+ implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
+// implementation 'io.jsonwebtoken:jjwt:0.9.1'
+ implementation 'io.jsonwebtoken:jjwt-api:0.11.2'
+ implementation 'io.jsonwebtoken:jjwt-impl:0.11.2'
+ implementation 'io.jsonwebtoken:jjwt-jackson:0.11.2'
+ implementation 'org.springframework.boot:spring-boot-starter-quartz'
+
+ implementation "io.springfox:springfox-boot-starter:3.0.0"
+ implementation "io.springfox:springfox-swagger-ui:3.0.0"
+}
+
+test {
+ systemProperty 'user.timezone', 'Asia/Seoul'
+}
+
+tasks.named('test') {
+ outputs.dir snippetsDir
+ useJUnitPlatform()
+ finalizedBy jacocoTestReport
+}
+
+tasks.named('asciidoctor') {
+ inputs.dir snippetsDir
+ dependsOn test
+}
+
+/* QueryDSL setting start */
+def querydslDir = "$buildDir/generated/querydsl"
+
+querydsl {
+ jpa = true
+ querydslSourcesDir = querydslDir
+}
+
+sourceSets {
+ main.java.srcDir querydslDir
+}
+
+configurations {
+ complieOnly {
+ extendsFrom annotationProcessor
+ }
+ querydsl.extendsFrom compileClasspath
+}
+
+compileQuerydsl {
+ options.annotationProcessorPath = configurations.querydsl
+}
+/* QueryDSL setting end */
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..033e24c4
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..9f4197d5
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 00000000..fcb6fca1
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,248 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..6689b85b
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/lombok.config b/lombok.config
new file mode 100644
index 00000000..8f7e8aa1
--- /dev/null
+++ b/lombok.config
@@ -0,0 +1 @@
+lombok.addLombokGeneratedAnnotation = true
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..096502d2
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'server'
diff --git a/src/main/java/com/livable/server/LivableServerApplication.java b/src/main/java/com/livable/server/LivableServerApplication.java
new file mode 100644
index 00000000..487c7721
--- /dev/null
+++ b/src/main/java/com/livable/server/LivableServerApplication.java
@@ -0,0 +1,25 @@
+package com.livable.server;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+import javax.annotation.PostConstruct;
+import java.time.LocalDateTime;
+import java.util.TimeZone;
+
+@Slf4j
+@SpringBootApplication
+public class LivableServerApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(LivableServerApplication.class, args);
+ }
+
+ @PostConstruct
+ public void init() {
+ TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
+ log.info("프로젝트 세팅 시간: {}", LocalDateTime.now()); // TODO: 서버 로그 테스트 후 지울예정
+ }
+
+}
diff --git a/src/main/java/com/livable/server/admin/controller/AdminController.java b/src/main/java/com/livable/server/admin/controller/AdminController.java
new file mode 100644
index 00000000..0eed85f5
--- /dev/null
+++ b/src/main/java/com/livable/server/admin/controller/AdminController.java
@@ -0,0 +1,39 @@
+package com.livable.server.admin.controller;
+
+import com.livable.server.admin.domain.VisitationQuery;
+import com.livable.server.admin.service.AdminService;
+import com.livable.server.core.util.Actor;
+import com.livable.server.core.util.JwtTokenProvider;
+import com.livable.server.core.util.LoginActor;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/admin")
+public class AdminController {
+
+ private final AdminService adminService;
+
+ /**
+ * http://localhost:8080/api/admin/visitation?page=10&size=1&queryCondition=COMPANY&query=sixsense&startDate=2023-09-24&endDate=2023-09-25
+ * @param pageable
+ * @param visitationQueryCondition
+ */
+
+ @GetMapping("/visitation")
+ public ResponseEntity> getVisitationList(
+ Pageable pageable, VisitationQuery visitationQueryCondition, @LoginActor Actor actor
+ ) {
+
+ JwtTokenProvider.checkAdminToken(actor);
+
+ Long adminId = actor.getId();
+
+ return adminService.getVisitationList(pageable, visitationQueryCondition, adminId);
+ }
+}
diff --git a/src/main/java/com/livable/server/admin/domain/AdminErrorCode.java b/src/main/java/com/livable/server/admin/domain/AdminErrorCode.java
new file mode 100644
index 00000000..09599d2a
--- /dev/null
+++ b/src/main/java/com/livable/server/admin/domain/AdminErrorCode.java
@@ -0,0 +1,17 @@
+package com.livable.server.admin.domain;
+
+import com.livable.server.core.exception.ErrorCode;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public enum AdminErrorCode implements ErrorCode {
+ NOT_EXIST_ADMIN(HttpStatus.BAD_REQUEST, "존재하지 않는 관리자 입니다."),
+ INVALID_QUERY(HttpStatus.BAD_REQUEST, "검색 조건이 올바르지 않습니다.");
+
+ private final HttpStatus httpStatus;
+ private final String message;
+}
diff --git a/src/main/java/com/livable/server/admin/domain/VisitationQuery.java b/src/main/java/com/livable/server/admin/domain/VisitationQuery.java
new file mode 100644
index 00000000..1bfe8c46
--- /dev/null
+++ b/src/main/java/com/livable/server/admin/domain/VisitationQuery.java
@@ -0,0 +1,43 @@
+package com.livable.server.admin.domain;
+
+import com.livable.server.core.exception.GlobalRuntimeException;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.time.LocalDate;
+
+@Getter
+@NoArgsConstructor
+@Setter
+@ToString
+public class VisitationQuery {
+
+ private VisitationQueryCondition queryCondition;
+ private String query;
+ private LocalDate startDate;
+ private LocalDate endDate;
+
+ public void setDefaultDate() {
+ this.startDate = LocalDate.now();
+ this.endDate = LocalDate.now();
+ }
+
+ public void validate() {
+ if (startDate == null && endDate == null) {
+ setDefaultDate();
+ return;
+ }
+
+ if (startDate != null && endDate != null) {
+ checkDateTime(startDate, endDate);
+ }
+ }
+
+ private void checkDateTime(LocalDate startDate, LocalDate endDate) {
+ if (startDate.isAfter(endDate) || endDate.isBefore(startDate)) {
+ throw new GlobalRuntimeException(AdminErrorCode.INVALID_QUERY);
+ }
+ }
+}
diff --git a/src/main/java/com/livable/server/admin/domain/VisitationQueryCondition.java b/src/main/java/com/livable/server/admin/domain/VisitationQueryCondition.java
new file mode 100644
index 00000000..77385457
--- /dev/null
+++ b/src/main/java/com/livable/server/admin/domain/VisitationQueryCondition.java
@@ -0,0 +1,11 @@
+package com.livable.server.admin.domain;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+public enum VisitationQueryCondition {
+ COMPANY, VISITOR;
+}
diff --git a/src/main/java/com/livable/server/admin/dto/AdminResponse.java b/src/main/java/com/livable/server/admin/dto/AdminResponse.java
new file mode 100644
index 00000000..b63dd8c8
--- /dev/null
+++ b/src/main/java/com/livable/server/admin/dto/AdminResponse.java
@@ -0,0 +1,73 @@
+package com.livable.server.admin.dto;
+
+import lombok.*;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class AdminResponse {
+
+ @Getter
+ @Builder
+ public static class ListDTO {
+ private Long invitationId;
+ private String company;
+ private String host;
+ private Long visitorId;
+ private LocalDateTime startDateTime;
+ private LocalDateTime visitTime;
+ private String visitorName;
+ private String officeName;
+ private String carNumber;
+ private LocalDateTime inTime;
+ private LocalDateTime outTime;
+ private Integer stayTime;
+ }
+
+ @Getter
+ @AllArgsConstructor
+ public static class ProjectionForListDTO {
+ private Long invitationId;
+ private String company;
+ private String host;
+ private Long visitorId;
+ private LocalDate startDate;
+ private LocalTime startTime;
+ private LocalDateTime visitTime;
+ private String visitorName;
+ private String officeName;
+ private String carNumber;
+ private LocalDateTime inTime;
+ private LocalDateTime outTime;
+
+ public ListDTO toListDTO() {
+ return ListDTO.builder()
+ .invitationId(invitationId)
+ .company(company)
+ .host(host)
+ .visitorId(visitorId)
+ .startDateTime(LocalDateTime.of(startDate, startTime))
+ .visitTime(visitTime)
+ .visitorName(visitorName)
+ .officeName(officeName)
+ .carNumber(carNumber)
+ .inTime(inTime)
+ .outTime(outTime)
+ .stayTime(calculateStayTime(inTime, outTime))
+ .build();
+ }
+
+ private Integer calculateStayTime(LocalDateTime inTime, LocalDateTime outTime) {
+ if (inTime != null && outTime != null) {
+ Duration duration = Duration.between(inTime, outTime);
+
+ return Long.valueOf(duration.getSeconds()).intValue() / 60;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/main/java/com/livable/server/admin/repository/AdminQueryRepository.java b/src/main/java/com/livable/server/admin/repository/AdminQueryRepository.java
new file mode 100644
index 00000000..630b4ed1
--- /dev/null
+++ b/src/main/java/com/livable/server/admin/repository/AdminQueryRepository.java
@@ -0,0 +1,13 @@
+package com.livable.server.admin.repository;
+
+import com.livable.server.admin.domain.VisitationQuery;
+import com.livable.server.admin.dto.AdminResponse;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+
+public interface AdminQueryRepository {
+
+ Page findVisitationWithQuery(
+ Pageable pageable, VisitationQuery visitationQuery, Long buildingId
+ );
+}
diff --git a/src/main/java/com/livable/server/admin/repository/AdminQueryRepositoryImpl.java b/src/main/java/com/livable/server/admin/repository/AdminQueryRepositoryImpl.java
new file mode 100644
index 00000000..5f677287
--- /dev/null
+++ b/src/main/java/com/livable/server/admin/repository/AdminQueryRepositoryImpl.java
@@ -0,0 +1,98 @@
+package com.livable.server.admin.repository;
+
+
+import com.livable.server.admin.domain.VisitationQuery;
+import com.livable.server.admin.domain.VisitationQueryCondition;
+import com.livable.server.admin.dto.AdminResponse;
+import com.querydsl.core.BooleanBuilder;
+import com.querydsl.core.types.Projections;
+import com.querydsl.jpa.impl.JPAQuery;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageImpl;
+import org.springframework.data.domain.Pageable;
+import org.springframework.util.StringUtils;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import static com.livable.server.entity.QCompany.company;
+import static com.livable.server.entity.QParkingLog.parkingLog;
+import static com.livable.server.entity.QVisitor.visitor;
+import static com.livable.server.entity.QInvitation.invitation;
+import static com.livable.server.entity.QMember.member;
+import static com.livable.server.entity.QBuilding.building;
+
+@RequiredArgsConstructor
+public class AdminQueryRepositoryImpl implements AdminQueryRepository {
+
+ private final JPAQueryFactory queryFactory;
+
+ @Override
+ public Page findVisitationWithQuery(
+ Pageable pageable, VisitationQuery visitationQuery, Long buildingId
+ ) {
+
+ BooleanBuilder dateTimeBuilder = new BooleanBuilder();
+
+ if (visitationQuery.getStartDate() != null) {
+ dateTimeBuilder.and(invitation.startDate.goe(visitationQuery.getStartDate()));
+ }
+
+ if (visitationQuery.getEndDate() != null) {
+ dateTimeBuilder.and(invitation.startDate.loe(visitationQuery.getEndDate()));
+ }
+
+ BooleanBuilder queryBuilder = new BooleanBuilder();
+
+ if (visitationQuery.getQueryCondition() != null
+ && visitationQuery.getQueryCondition().equals(VisitationQueryCondition.VISITOR)
+ && StringUtils.hasText(visitationQuery.getQuery())) {
+ queryBuilder.and(visitor.name.contains(visitationQuery.getQuery()));
+ }
+
+ if (visitationQuery.getQueryCondition() != null
+ && visitationQuery.getQueryCondition().equals(VisitationQueryCondition.COMPANY)
+ && StringUtils.hasText(visitationQuery.getQuery())) {
+ queryBuilder.and(company.name.contains(visitationQuery.getQuery()));
+ }
+
+ JPAQuery query =
+ queryFactory.select(Projections.constructor(AdminResponse.ProjectionForListDTO.class,
+ invitation.id,
+ company.name,
+ member.name,
+ visitor.id,
+ invitation.startDate,
+ invitation.startTime,
+ visitor.firstVisitedTime,
+ visitor.name,
+ invitation.officeName,
+ parkingLog.carNumber,
+ parkingLog.inTime,
+ parkingLog.outTime
+ ))
+ .from(visitor)
+ .leftJoin(parkingLog).on(parkingLog.visitor.id.eq(visitor.id))
+ .join(invitation).on(dateTimeBuilder.and(visitor.invitation.id.eq(invitation.id)))
+ .join(member).on(invitation.member.id.eq(member.id))
+ .join(company).on(member.company.id.eq(company.id))
+ .join(building).on(company.building.id.eq(building.id), building.id.eq(buildingId))
+ .where(queryBuilder)
+ .orderBy(invitation.startDate.asc());
+
+ List projection = query.offset(pageable.getOffset())
+ .limit(pageable.getPageSize())
+ .fetch();
+
+ List content = projection.stream()
+ .map(AdminResponse.ProjectionForListDTO::toListDTO)
+ .collect(Collectors.toList());
+
+ long count = query.fetchCount();
+
+
+ return new PageImpl<>(content, pageable, count);
+ }
+}
diff --git a/src/main/java/com/livable/server/admin/repository/AdminRepository.java b/src/main/java/com/livable/server/admin/repository/AdminRepository.java
new file mode 100644
index 00000000..c6cf0b3b
--- /dev/null
+++ b/src/main/java/com/livable/server/admin/repository/AdminRepository.java
@@ -0,0 +1,8 @@
+package com.livable.server.admin.repository;
+
+import com.livable.server.entity.Admin;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+
+public interface AdminRepository extends JpaRepository, AdminQueryRepository {
+}
diff --git a/src/main/java/com/livable/server/admin/service/AdminService.java b/src/main/java/com/livable/server/admin/service/AdminService.java
new file mode 100644
index 00000000..fe391850
--- /dev/null
+++ b/src/main/java/com/livable/server/admin/service/AdminService.java
@@ -0,0 +1,41 @@
+package com.livable.server.admin.service;
+
+import com.livable.server.admin.domain.AdminErrorCode;
+import com.livable.server.admin.domain.VisitationQuery;
+import com.livable.server.admin.dto.AdminResponse;
+import com.livable.server.admin.repository.AdminRepository;
+import com.livable.server.core.exception.GlobalRuntimeException;
+import com.livable.server.core.response.ApiResponse;
+import com.livable.server.entity.Admin;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Optional;
+
+@RequiredArgsConstructor
+@Service
+public class AdminService {
+
+ private final AdminRepository adminRepository;
+
+ @Transactional(readOnly = true)
+ public ResponseEntity> getVisitationList(
+ Pageable pageable, VisitationQuery visitationQuery, Long adminId
+ ) {
+ visitationQuery.validate();
+
+ Optional optionalAdmin = adminRepository.findById(adminId);
+ Admin admin = optionalAdmin.orElseThrow(
+ () -> new GlobalRuntimeException(AdminErrorCode.NOT_EXIST_ADMIN));
+
+ Page responseBody
+ = adminRepository.findVisitationWithQuery(pageable, visitationQuery, admin.getBuilding().getId());
+
+ return ApiResponse.success(responseBody, HttpStatus.OK);
+ }
+}
diff --git a/src/main/java/com/livable/server/core/config/JpaConfig.java b/src/main/java/com/livable/server/core/config/JpaConfig.java
new file mode 100644
index 00000000..8bd76345
--- /dev/null
+++ b/src/main/java/com/livable/server/core/config/JpaConfig.java
@@ -0,0 +1,9 @@
+package com.livable.server.core.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
+
+@Configuration
+@EnableJpaAuditing
+public class JpaConfig {
+}
diff --git a/src/main/java/com/livable/server/core/config/QueryDslConfig.java b/src/main/java/com/livable/server/core/config/QueryDslConfig.java
new file mode 100644
index 00000000..5f5b0518
--- /dev/null
+++ b/src/main/java/com/livable/server/core/config/QueryDslConfig.java
@@ -0,0 +1,16 @@
+package com.livable.server.core.config;
+
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.persistence.EntityManager;
+
+@Configuration
+public class QueryDslConfig {
+
+ @Bean
+ public JPAQueryFactory jpaQueryFactory(EntityManager em) {
+ return new JPAQueryFactory(em);
+ }
+}
diff --git a/src/main/java/com/livable/server/core/config/S3Config.java b/src/main/java/com/livable/server/core/config/S3Config.java
new file mode 100644
index 00000000..2a633de0
--- /dev/null
+++ b/src/main/java/com/livable/server/core/config/S3Config.java
@@ -0,0 +1,34 @@
+package com.livable.server.core.config;
+
+import com.amazonaws.auth.AWSCredentials;
+import com.amazonaws.auth.AWSStaticCredentialsProvider;
+import com.amazonaws.auth.BasicAWSCredentials;
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.AmazonS3ClientBuilder;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class S3Config {
+
+ @Value("${cloud.aws.credentials.accessKey}")
+ private String accessKey;
+
+ @Value("${cloud.aws.credentials.secretKey}")
+ private String secretKey;
+
+ @Value("${cloud.aws.region.static}")
+ private String region;
+
+ @Bean
+ public AmazonS3 amazonS3Client() {
+ AWSCredentials credentials = new BasicAWSCredentials(accessKey, secretKey);
+
+ return AmazonS3ClientBuilder
+ .standard()
+ .withCredentials(new AWSStaticCredentialsProvider(credentials))
+ .withRegion(region)
+ .build();
+ }
+}
diff --git a/src/main/java/com/livable/server/core/config/SwaggerConfig.java b/src/main/java/com/livable/server/core/config/SwaggerConfig.java
new file mode 100644
index 00000000..70ecd9fd
--- /dev/null
+++ b/src/main/java/com/livable/server/core/config/SwaggerConfig.java
@@ -0,0 +1,37 @@
+package com.livable.server.core.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import springfox.documentation.builders.ApiInfoBuilder;
+import springfox.documentation.builders.PathSelectors;
+import springfox.documentation.builders.RequestHandlerSelectors;
+import springfox.documentation.service.ApiInfo;
+import springfox.documentation.spi.DocumentationType;
+import springfox.documentation.spring.web.plugins.Docket;
+
+@Configuration
+public class SwaggerConfig {
+
+ private static final String SERVICE_NAME = "오피스너 - 식스센스";
+ private static final String API_VERSION = "V1";
+ private static final String API_DESCRIPTION = "오피스너";
+ private static final String API_URL = "https://livableserver.site/";
+
+ @Bean
+ public Docket api() {
+ return new Docket(DocumentationType.OAS_30)
+ .apiInfo(apiInfo())
+ .select()
+ .apis(RequestHandlerSelectors.basePackage("com.livable.server"))
+ .paths(PathSelectors.any())
+ .build();
+ }
+
+ private ApiInfo apiInfo() {
+ return new ApiInfoBuilder().title(SERVICE_NAME)
+ .version(API_VERSION)
+ .description(API_DESCRIPTION)
+ .termsOfServiceUrl(API_URL)
+ .build();
+ }
+}
diff --git a/src/main/java/com/livable/server/core/config/WebConfig.java b/src/main/java/com/livable/server/core/config/WebConfig.java
new file mode 100644
index 00000000..1c84f0ee
--- /dev/null
+++ b/src/main/java/com/livable/server/core/config/WebConfig.java
@@ -0,0 +1,43 @@
+package com.livable.server.core.config;
+
+import com.livable.server.core.util.JwtTokenProvider;
+import com.livable.server.core.util.LoginActorArgumentResolver;
+import com.livable.server.core.util.StringToLocalDateConverter;
+import com.livable.server.core.util.StringToRestaurantCategoryConverter;
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.format.FormatterRegistry;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.List;
+
+@RequiredArgsConstructor
+@Configuration
+public class WebConfig implements WebMvcConfigurer {
+
+ private final JwtTokenProvider tokenProvider;
+
+
+ @Override
+ public void addFormatters(FormatterRegistry registry) {
+ registry.addConverter(new StringToRestaurantCategoryConverter());
+ registry.addConverter(new StringToLocalDateConverter());
+ }
+
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/**")
+ .allowedHeaders("*")
+ .allowedMethods("*")
+ .allowCredentials(true)
+ .exposedHeaders("Authorization")
+ .allowedOriginPatterns("*");
+ }
+
+ @Override
+ public void addArgumentResolvers(List resolvers) {
+ resolvers.add(new LoginActorArgumentResolver(tokenProvider));
+ }
+}
diff --git a/src/main/java/com/livable/server/core/exception/ErrorCode.java b/src/main/java/com/livable/server/core/exception/ErrorCode.java
new file mode 100644
index 00000000..cadd9c54
--- /dev/null
+++ b/src/main/java/com/livable/server/core/exception/ErrorCode.java
@@ -0,0 +1,10 @@
+package com.livable.server.core.exception;
+
+import org.springframework.http.HttpStatus;
+
+public interface ErrorCode {
+
+ String getMessage();
+
+ HttpStatus getHttpStatus();
+}
diff --git a/src/main/java/com/livable/server/core/exception/GlobalErrorCode.java b/src/main/java/com/livable/server/core/exception/GlobalErrorCode.java
new file mode 100644
index 00000000..8cdfe15b
--- /dev/null
+++ b/src/main/java/com/livable/server/core/exception/GlobalErrorCode.java
@@ -0,0 +1,15 @@
+package com.livable.server.core.exception;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@AllArgsConstructor
+public enum GlobalErrorCode implements ErrorCode {
+
+ INVALID_TYPE(HttpStatus.BAD_REQUEST, "입력 형식이 올바르지 않습니다");
+
+ private final HttpStatus httpStatus;
+ private final String message;
+}
diff --git a/src/main/java/com/livable/server/core/exception/GlobalExceptionHandler.java b/src/main/java/com/livable/server/core/exception/GlobalExceptionHandler.java
new file mode 100644
index 00000000..455ba006
--- /dev/null
+++ b/src/main/java/com/livable/server/core/exception/GlobalExceptionHandler.java
@@ -0,0 +1,48 @@
+package com.livable.server.core.exception;
+
+import com.livable.server.core.response.ApiResponse;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.validation.BindException;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
+
+@Slf4j
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(MethodArgumentTypeMismatchException.class)
+ public ResponseEntity methodMethodArgumentTypeMismatchExceptionHandle(MethodArgumentTypeMismatchException e) {
+ return ApiResponse.error(GlobalErrorCode.INVALID_TYPE.getMessage(), HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(BindException.class)
+ public ResponseEntity bindException(BindException e) {
+ log.error("bindException", e);
+
+ return ApiResponse.error(e.getBindingResult().getFieldError().getDefaultMessage(), HttpStatus.BAD_REQUEST);
+ }
+
+ @ExceptionHandler(GlobalRuntimeException.class)
+ public ResponseEntity globalRuntimeExceptionHandle(GlobalRuntimeException e) {
+ log.error("globalRuntimeExceptionHandle", e);
+
+ return ApiResponse.error(e.getErrorCode());
+ }
+
+ @ExceptionHandler(RuntimeException.class)
+ public ResponseEntity runtimeExceptionHandle(RuntimeException e) {
+ log.error("runtimeExceptionHandle", e);
+
+ return ApiResponse.error(e.getCause().getCause().getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity exceptionHandle(Exception e) {
+ log.error("exceptionHandle", e);
+
+ return ApiResponse.error(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+}
diff --git a/src/main/java/com/livable/server/core/exception/GlobalRuntimeException.java b/src/main/java/com/livable/server/core/exception/GlobalRuntimeException.java
new file mode 100644
index 00000000..7b27b17d
--- /dev/null
+++ b/src/main/java/com/livable/server/core/exception/GlobalRuntimeException.java
@@ -0,0 +1,14 @@
+package com.livable.server.core.exception;
+
+import lombok.Getter;
+
+@Getter
+public class GlobalRuntimeException extends RuntimeException {
+
+ private final ErrorCode errorCode;
+
+ public GlobalRuntimeException(ErrorCode errorCode) {
+ super(errorCode.getMessage());
+ this.errorCode = errorCode;
+ }
+}
diff --git a/src/main/java/com/livable/server/core/response/ApiResponse.java b/src/main/java/com/livable/server/core/response/ApiResponse.java
new file mode 100644
index 00000000..eeeafc81
--- /dev/null
+++ b/src/main/java/com/livable/server/core/response/ApiResponse.java
@@ -0,0 +1,56 @@
+package com.livable.server.core.response;
+
+import com.livable.server.core.exception.ErrorCode;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.lang.NonNull;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class ApiResponse {
+
+ public static ResponseEntity> success(@NonNull HttpStatus httpStatus) {
+ return ResponseEntity.status(httpStatus)
+ .build();
+ }
+
+ public static ResponseEntity> success(@NonNull T data, @NonNull HttpStatus httpStatus) {
+ return ResponseEntity.status(httpStatus)
+ .body(Success.of(data));
+ }
+
+ public static ResponseEntity error(@NonNull String message, @NonNull HttpStatus httpStatus) {
+ return ResponseEntity.status(httpStatus)
+ .body(Error.of(message));
+ }
+
+ public static ResponseEntity error(@NonNull ErrorCode errorCode) {
+ return ResponseEntity.status(errorCode.getHttpStatus())
+ .body(Error.of(errorCode.getMessage()));
+ }
+
+ @Getter
+ @AllArgsConstructor(access = AccessLevel.PRIVATE)
+ public static class Error {
+
+ private String message;
+
+ public static Error of(@NonNull String errorMessage) {
+ return new Error(errorMessage);
+ }
+ }
+
+ @Getter
+ @AllArgsConstructor(access = AccessLevel.PRIVATE)
+ public static class Success {
+
+ private T data;
+
+ public static Success of(@NonNull T data) {
+ return new Success<>(data);
+ }
+ }
+}
diff --git a/src/main/java/com/livable/server/core/util/Actor.java b/src/main/java/com/livable/server/core/util/Actor.java
new file mode 100644
index 00000000..0978c3c0
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/Actor.java
@@ -0,0 +1,12 @@
+package com.livable.server.core.util;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class Actor {
+
+ private Long id;
+ private ActorType actorType;
+}
diff --git a/src/main/java/com/livable/server/core/util/ActorType.java b/src/main/java/com/livable/server/core/util/ActorType.java
new file mode 100644
index 00000000..e3f89125
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/ActorType.java
@@ -0,0 +1,18 @@
+package com.livable.server.core.util;
+
+import com.livable.server.core.exception.GlobalRuntimeException;
+import com.livable.server.member.domain.MemberErrorCode;
+
+import java.util.Arrays;
+
+public enum ActorType {
+ MEMBER, VISITOR, ADMIN;
+
+ public static ActorType of(String type) {
+ return Arrays.stream(values())
+ .filter(actorType -> actorType.name().equals(type))
+ .findFirst()
+ .orElseThrow(() -> new GlobalRuntimeException(MemberErrorCode.INVALID_ACTOR_TYPE));
+ }
+}
+
diff --git a/src/main/java/com/livable/server/core/util/ImageSeparator.java b/src/main/java/com/livable/server/core/util/ImageSeparator.java
new file mode 100644
index 00000000..0e6b92c9
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/ImageSeparator.java
@@ -0,0 +1,26 @@
+package com.livable.server.core.util;
+
+import org.springframework.stereotype.Component;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+@Component
+public class ImageSeparator {
+
+ public static final String IMAGE_SEPARATOR = ",";
+
+ /**
+ * IMAGE_SEPARATOR로 이어진 이미지 url 문자열을 리스트로 분리하여 반환한다.
+ * @param concatenatedImageUrl
+ * @return 분리된 url 리스트
+ */
+ public List separateConcatenatedImages(String concatenatedImageUrl) {
+ if (Objects.isNull(concatenatedImageUrl)) {
+ return new ArrayList<>();
+ }
+ return Arrays.asList(concatenatedImageUrl.split(IMAGE_SEPARATOR));
+ }
+}
diff --git a/src/main/java/com/livable/server/core/util/JwtTokenProvider.java b/src/main/java/com/livable/server/core/util/JwtTokenProvider.java
new file mode 100644
index 00000000..2a44ad2d
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/JwtTokenProvider.java
@@ -0,0 +1,82 @@
+package com.livable.server.core.util;
+
+import com.livable.server.core.exception.GlobalRuntimeException;
+import com.livable.server.member.domain.MemberErrorCode;
+import io.jsonwebtoken.Claims;
+import io.jsonwebtoken.Jws;
+import io.jsonwebtoken.JwtException;
+import io.jsonwebtoken.Jwts;
+import io.jsonwebtoken.security.Keys;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+
+import java.security.Key;
+import java.util.Date;
+
+@Component
+public class JwtTokenProvider {
+
+ private final Key secretKey;
+
+ public JwtTokenProvider(@Value("${jwt.secret}") String secretKey) {
+ this.secretKey = Keys.hmacShaKeyFor(secretKey.getBytes());
+ }
+
+ public String createActorToken(ActorType actorType, Long actorId, Date expireDate) {
+
+ Claims claims = Jwts.claims();
+ claims.put("actorId", actorId);
+ claims.put("actorType", actorType);
+
+ return Jwts.builder()
+ .setClaims(claims)
+ .setExpiration(expireDate)
+ .signWith(secretKey)
+ .compact();
+ }
+
+ public boolean isValidateToken(String token) {
+ try {
+ Jws claimsJws = Jwts.parserBuilder()
+ .setSigningKey(secretKey)
+ .build()
+ .parseClaimsJws(token);
+
+ return claimsJws.getBody().getExpiration().after(new Date());
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public Claims parseClaims(String token) {
+ try {
+ return Jwts.parserBuilder()
+ .setSigningKey(secretKey)
+ .build()
+ .parseClaimsJws(token)
+ .getBody();
+ } catch (JwtException e) {
+ e.printStackTrace();
+ throw new GlobalRuntimeException(MemberErrorCode.INVALID_TOKEN);
+ }
+ }
+
+ public static void checkMemberToken(Actor actor) {
+ if (actor.getActorType() != ActorType.MEMBER) {
+ throw new GlobalRuntimeException(MemberErrorCode.INVALID_TOKEN);
+ }
+ }
+
+ public static void checkVisitorToken(Actor actor) {
+ if (actor.getActorType() != ActorType.VISITOR) {
+ throw new GlobalRuntimeException(MemberErrorCode.INVALID_TOKEN);
+ }
+ }
+
+ public static void checkAdminToken(Actor actor) {
+ if (actor.getActorType() != ActorType.ADMIN) {
+ throw new GlobalRuntimeException(MemberErrorCode.INVALID_TOKEN);
+ }
+ }
+
+}
diff --git a/src/main/java/com/livable/server/core/util/LoginActor.java b/src/main/java/com/livable/server/core/util/LoginActor.java
new file mode 100644
index 00000000..8147e17c
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/LoginActor.java
@@ -0,0 +1,11 @@
+package com.livable.server.core.util;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface LoginActor {
+}
diff --git a/src/main/java/com/livable/server/core/util/LoginActorArgumentResolver.java b/src/main/java/com/livable/server/core/util/LoginActorArgumentResolver.java
new file mode 100644
index 00000000..e5dadddb
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/LoginActorArgumentResolver.java
@@ -0,0 +1,43 @@
+package com.livable.server.core.util;
+
+import io.jsonwebtoken.Claims;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.MethodParameter;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+@RequiredArgsConstructor
+@Slf4j
+public class LoginActorArgumentResolver implements HandlerMethodArgumentResolver {
+
+ private final JwtTokenProvider tokenProvider;
+
+
+ @Override
+ public boolean supportsParameter(MethodParameter parameter) {
+ return parameter.hasParameterAnnotation(LoginActor.class) && parameter.getParameterType().equals(Actor.class);
+ }
+
+ @Override
+ public Object resolveArgument(
+ MethodParameter parameter,
+ ModelAndViewContainer mavContainer,
+ NativeWebRequest webRequest,
+ WebDataBinderFactory binderFactory
+ ) throws Exception {
+ String authorization = webRequest.getHeader("Authorization");
+ String token = authorization.split("Bearer ")[1];
+
+ Claims claims = tokenProvider.parseClaims(token);
+ Long actorId = claims.get("actorId", Long.class);
+ String actorType = claims.get("actorType", String.class);
+
+ return Actor.builder()
+ .id(actorId)
+ .actorType(ActorType.of(actorType))
+ .build();
+ }
+}
diff --git a/src/main/java/com/livable/server/core/util/PresentDate.java b/src/main/java/com/livable/server/core/util/PresentDate.java
new file mode 100644
index 00000000..a151d091
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/PresentDate.java
@@ -0,0 +1,19 @@
+package com.livable.server.core.util;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import javax.validation.Constraint;
+import javax.validation.Payload;
+
+@Documented
+@Constraint(validatedBy = PresentDateValidator.class)
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface PresentDate {
+ String message() default "Date는 오늘 날짜와 같아야 합니다.";
+ Class>[] groups() default {};
+ Class extends Payload>[] payload() default {};
+}
\ No newline at end of file
diff --git a/src/main/java/com/livable/server/core/util/PresentDateValidator.java b/src/main/java/com/livable/server/core/util/PresentDateValidator.java
new file mode 100644
index 00000000..35a2096c
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/PresentDateValidator.java
@@ -0,0 +1,21 @@
+package com.livable.server.core.util;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class PresentDateValidator implements ConstraintValidator {
+ @Override
+ public boolean isValid(LocalDate date, ConstraintValidatorContext context) {
+
+ if (date == null) {
+ return false;
+ }
+
+ LocalDate currentDate = LocalDateTime.now().toLocalDate();
+
+ return date.isEqual(currentDate);
+
+ }
+}
diff --git a/src/main/java/com/livable/server/core/util/S3Uploader.java b/src/main/java/com/livable/server/core/util/S3Uploader.java
new file mode 100644
index 00000000..9f95b371
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/S3Uploader.java
@@ -0,0 +1,80 @@
+package com.livable.server.core.util;
+
+import com.amazonaws.services.s3.AmazonS3;
+import com.amazonaws.services.s3.model.ObjectMetadata;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.UUID;
+
+@RequiredArgsConstructor
+@Component
+public class S3Uploader {
+
+ private static final List ALLOWED_EXTENSIONS = List.of("jpg", "png", "gif", "jpeg");
+ private static final String EXTENSION_SEPARATOR = ".";
+
+ private final AmazonS3 amazonS3;
+
+ @Value("${cloud.aws.s3.bucket}")
+ private String bucket;
+
+ public List saveFile(List files) throws IOException {
+ if (Objects.isNull(files) || files.isEmpty()) {
+ return new ArrayList<>();
+ }
+
+ List accessUrls = new ArrayList<>();
+
+ for (MultipartFile file : files) {
+ String accessUrl = saveFile(file);
+ accessUrls.add(accessUrl);
+ }
+ return accessUrls;
+ }
+
+ public String saveFile(MultipartFile file) throws IOException {
+
+ String filename = file.getOriginalFilename();
+ final String originalFileName = filename.replaceAll(ImageSeparator.IMAGE_SEPARATOR, "");
+
+ final String fileExtension = getFileExtension(originalFileName);
+
+ validationAllowedFileExtension(fileExtension);
+ String randomFileName = generateRandomFileName(originalFileName, fileExtension);
+
+ ObjectMetadata metadata = new ObjectMetadata();
+ metadata.setContentLength(file.getSize());
+ metadata.setContentType(file.getContentType());
+
+ amazonS3.putObject(bucket, randomFileName, file.getInputStream(), metadata);
+
+ return amazonS3.getUrl(bucket, randomFileName).toString();
+ }
+
+ // 랜덤한 파일 이름을 생성하는 메서드
+ private String generateRandomFileName(String originalFileName, String fileExtension) {
+ return UUID.randomUUID() + originalFileName + EXTENSION_SEPARATOR + fileExtension;
+ }
+
+ // 파일 이름에서 파일 확장자를 추출하는 메서드
+ private String getFileExtension(String originalFileName) {
+ return originalFileName
+ .substring(originalFileName.lastIndexOf(EXTENSION_SEPARATOR) + 1)
+ .toLowerCase();
+
+ }
+
+ // 파일 확장자를 검증하는 메서드
+ private void validationAllowedFileExtension(String fileExtension) {
+ if (!ALLOWED_EXTENSIONS.contains(fileExtension)) {
+ throw new IllegalArgumentException("지원하지 않는 파일 확장자 입니다.");
+ }
+ }
+}
diff --git a/src/main/java/com/livable/server/core/util/StringToLocalDateConverter.java b/src/main/java/com/livable/server/core/util/StringToLocalDateConverter.java
new file mode 100644
index 00000000..7c1ed921
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/StringToLocalDateConverter.java
@@ -0,0 +1,21 @@
+package com.livable.server.core.util;
+
+import com.livable.server.core.exception.GlobalErrorCode;
+import com.livable.server.core.exception.GlobalRuntimeException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.convert.converter.Converter;
+
+import java.time.LocalDate;
+
+@Slf4j
+public class StringToLocalDateConverter implements Converter {
+
+ @Override
+ public LocalDate convert(String source) {
+ try {
+ return LocalDate.parse(source);
+ } catch (RuntimeException e) {
+ throw new GlobalRuntimeException(GlobalErrorCode.INVALID_TYPE);
+ }
+ }
+}
diff --git a/src/main/java/com/livable/server/core/util/StringToRestaurantCategoryConverter.java b/src/main/java/com/livable/server/core/util/StringToRestaurantCategoryConverter.java
new file mode 100644
index 00000000..824574ec
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/StringToRestaurantCategoryConverter.java
@@ -0,0 +1,12 @@
+package com.livable.server.core.util;
+
+import com.livable.server.entity.RestaurantCategory;
+import org.springframework.core.convert.converter.Converter;
+
+public class StringToRestaurantCategoryConverter implements Converter {
+
+ @Override
+ public RestaurantCategory convert(String event) {
+ return RestaurantCategory.of(event);
+ }
+}
diff --git a/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/domain/CronSchedule.java b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/domain/CronSchedule.java
new file mode 100644
index 00000000..422f74cb
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/domain/CronSchedule.java
@@ -0,0 +1,14 @@
+package com.livable.server.core.util.scheduler.menuchoiceresult.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum CronSchedule {
+
+ EVERY_DAY_OF_ZERO("0 0 0 1/1 * ? *"),
+ EVERY_SUNDAY_OF_END("0 45 23 ? * SUN *");
+
+ private final String croneSchedule;
+}
diff --git a/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultDailyJob.java b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultDailyJob.java
new file mode 100644
index 00000000..87c3dce8
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultDailyJob.java
@@ -0,0 +1,38 @@
+package com.livable.server.core.util.scheduler.menuchoiceresult.job;
+
+import com.livable.server.menu.service.MenuChoiceResultService;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+
+@RequiredArgsConstructor
+@Slf4j
+public class MenuChoiceResultDailyJob implements Job {
+
+ private final MenuChoiceResultService menuChoiceResultService;
+
+ @Override
+ public void execute(JobExecutionContext context) {
+
+ //Date of Job executed
+ Long milliseconds = context.getFireTime().getTime();
+
+ LocalDate referenceDate = millisecondsToDate(milliseconds);
+
+ menuChoiceResultService.createDailyMenuChoiceResult(referenceDate);
+
+ }
+
+ private LocalDate millisecondsToDate(Long milliseconds) {
+ Instant instant = Instant.ofEpochMilli(milliseconds);
+
+ ZoneId zoneId = ZoneId.systemDefault();
+ return instant.atZone(zoneId).toLocalDate();
+ }
+
+
+}
diff --git a/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultWeeklyJob.java b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultWeeklyJob.java
new file mode 100644
index 00000000..28627a9a
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/job/MenuChoiceResultWeeklyJob.java
@@ -0,0 +1,37 @@
+package com.livable.server.core.util.scheduler.menuchoiceresult.job;
+
+import com.livable.server.menu.service.MenuChoiceResultService;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.quartz.Job;
+import org.quartz.JobExecutionContext;
+
+@RequiredArgsConstructor
+@Slf4j
+public class MenuChoiceResultWeeklyJob implements Job {
+
+ private final MenuChoiceResultService menuChoiceResultService;
+
+ @Override
+ public void execute(JobExecutionContext context) {
+
+ //Date of Job executed
+ Long milliseconds = context.getFireTime().getTime();
+
+ LocalDate referenceDate = millisecondsToDate(milliseconds);
+
+ menuChoiceResultService.createWeeklyMenuChoiceResult(referenceDate);
+
+ }
+ private LocalDate millisecondsToDate(Long milliseconds) {
+ Instant instant = Instant.ofEpochMilli(milliseconds);
+
+ ZoneId zoneId = ZoneId.systemDefault();
+ return instant.atZone(zoneId).toLocalDate();
+ }
+
+
+}
diff --git a/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceJobListener.java b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceJobListener.java
new file mode 100644
index 00000000..61fead2f
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceJobListener.java
@@ -0,0 +1,35 @@
+package com.livable.server.core.util.scheduler.menuchoiceresult.listener;
+
+import lombok.extern.slf4j.Slf4j;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobExecutionException;
+import org.quartz.JobKey;
+import org.springframework.stereotype.Component;
+
+@Component
+@Slf4j
+public class MenuChoiceJobListener implements org.quartz.JobListener {
+
+ @Override
+ public String getName() {
+ return MenuChoiceJobListener.class.getName();
+ }
+
+ @Override
+ public void jobToBeExecuted(JobExecutionContext context) {
+ JobKey jobKey = context.getJobDetail().getKey();
+ log.info("Job Executed : JobKey : {}",jobKey);
+ }
+
+ @Override
+ public void jobExecutionVetoed(JobExecutionContext context) {
+ JobKey jobKey = context.getJobDetail().getKey();
+ log.info("Job ExecutionVetoed : JobKey : {}",jobKey);
+ }
+
+ @Override
+ public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) {
+ JobKey jobKey = context.getJobDetail().getKey();
+ log.info("Job Was Executed : JobKey : {}",jobKey);
+ }
+}
diff --git a/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceTriggerListener.java b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceTriggerListener.java
new file mode 100644
index 00000000..0848aec6
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/listener/MenuChoiceTriggerListener.java
@@ -0,0 +1,42 @@
+package com.livable.server.core.util.scheduler.menuchoiceresult.listener;
+
+import lombok.extern.slf4j.Slf4j;
+import org.quartz.JobExecutionContext;
+import org.quartz.JobKey;
+import org.quartz.Trigger;
+import org.quartz.Trigger.CompletedExecutionInstruction;
+import org.springframework.stereotype.Component;
+
+@Component
+@Slf4j
+public class MenuChoiceTriggerListener implements org.quartz.TriggerListener {
+
+ @Override
+ public String getName() {
+ return MenuChoiceTriggerListener.class.getName();
+ }
+
+ @Override
+ public void triggerFired(Trigger trigger, JobExecutionContext context) {
+ JobKey jobKey = trigger.getJobKey();
+ log.info("Trigger Fired at {} : JobKey : {}", trigger.getStartTime(),jobKey);
+ }
+
+ @Override
+ public boolean vetoJobExecution(Trigger trigger, JobExecutionContext context) {
+ return false;
+ }
+
+ @Override
+ public void triggerMisfired(Trigger trigger) {
+ JobKey jobKey = trigger.getJobKey();
+ log.info("Trigger MisFired at {} : JobKey : {}", trigger.getStartTime(),jobKey);
+ }
+
+ @Override
+ public void triggerComplete(Trigger trigger, JobExecutionContext context,
+ CompletedExecutionInstruction triggerInstructionCode) {
+ JobKey jobKey = trigger.getJobKey();
+ log.info("Trigger Complete at {} : JobKey : {}", trigger.getStartTime(),jobKey);
+ }
+}
diff --git a/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/service/MenuChoiceQuartzService.java b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/service/MenuChoiceQuartzService.java
new file mode 100644
index 00000000..bbd5220d
--- /dev/null
+++ b/src/main/java/com/livable/server/core/util/scheduler/menuchoiceresult/service/MenuChoiceQuartzService.java
@@ -0,0 +1,81 @@
+package com.livable.server.core.util.scheduler.menuchoiceresult.service;
+
+import static org.quartz.CronScheduleBuilder.cronSchedule;
+
+import com.livable.server.core.util.scheduler.menuchoiceresult.domain.CronSchedule;
+import com.livable.server.core.util.scheduler.menuchoiceresult.job.MenuChoiceResultWeeklyJob;
+import com.livable.server.core.util.scheduler.menuchoiceresult.listener.MenuChoiceJobListener;
+import com.livable.server.core.util.scheduler.menuchoiceresult.listener.MenuChoiceTriggerListener;
+import com.livable.server.core.util.scheduler.menuchoiceresult.job.MenuChoiceResultDailyJob;
+import java.util.TimeZone;
+import java.util.UUID;
+import javax.annotation.PostConstruct;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.quartz.CronTrigger;
+import org.quartz.Job;
+import org.quartz.JobBuilder;
+import org.quartz.JobDetail;
+import org.quartz.ListenerManager;
+import org.quartz.Scheduler;
+import org.quartz.SchedulerException;
+import org.quartz.TriggerBuilder;
+import org.springframework.scheduling.quartz.SchedulerFactoryBean;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class MenuChoiceQuartzService {
+
+ private final SchedulerFactoryBean schedulerFactory;
+
+ private static final String GROUP_NAME = "Menu Choice Result";
+
+ @PostConstruct
+ public void scheduled() {
+
+ Scheduler scheduler = schedulerFactory.getScheduler();
+
+ JobDetail menuChoiceResultDailyJob = menuChoiceResultJobDetail(MenuChoiceResultDailyJob.class);
+ CronTrigger menuChoiceResultDailyCronTrigger = menuChoiceResultCronTrigger(menuChoiceResultDailyJob, CronSchedule.EVERY_DAY_OF_ZERO.getCroneSchedule(), "가장 많이 선택된 메뉴 집계 Daily Trigger");
+
+ JobDetail menuChoiceResultWeeklyJob = menuChoiceResultJobDetail(MenuChoiceResultWeeklyJob.class);
+ CronTrigger menuChoiceResultWeeklyCronTrigger = menuChoiceResultCronTrigger(menuChoiceResultWeeklyJob, CronSchedule.EVERY_SUNDAY_OF_END.getCroneSchedule(), "가장 많이 선택된 메뉴 집계 WeeklyTrigger");
+
+ try {
+ //Listener Setting
+ ListenerManager listenerManager = scheduler.getListenerManager();
+ listenerManager.addJobListener(new MenuChoiceJobListener());
+ listenerManager.addTriggerListener(new MenuChoiceTriggerListener());
+
+ //Add Job & Trigger to schedule
+ scheduler.scheduleJob(menuChoiceResultDailyJob, menuChoiceResultDailyCronTrigger);
+ scheduler.scheduleJob(menuChoiceResultWeeklyJob, menuChoiceResultWeeklyCronTrigger);
+
+ } catch (SchedulerException e) {
+ log.error("Menu Choice Result Scheduler Exception Occurred : {}", menuChoiceResultDailyJob.getKey());
+ }
+ }
+
+ public JobDetail menuChoiceResultJobDetail(Class extends Job> clazz) {
+
+ return JobBuilder.newJob(clazz)
+ .withIdentity(UUID.randomUUID().toString(), GROUP_NAME)
+ .withDescription(clazz.getSimpleName())
+ .build();
+ }
+
+ public CronTrigger menuChoiceResultCronTrigger(JobDetail jobDetail, String schedule, String description) {
+ return TriggerBuilder.newTrigger()
+ .forJob(jobDetail)
+ .withIdentity(jobDetail.getKey().getName(), GROUP_NAME)
+ .withDescription(description)
+ .withSchedule(cronSchedule(schedule)
+ .inTimeZone(TimeZone.getDefault())
+ .withMisfireHandlingInstructionFireAndProceed()
+ )
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/livable/server/entity/Admin.java b/src/main/java/com/livable/server/entity/Admin.java
new file mode 100644
index 00000000..b9f16c99
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Admin.java
@@ -0,0 +1,20 @@
+package com.livable.server.entity;
+
+import lombok.Getter;
+
+import javax.persistence.*;
+
+@Getter
+@Entity
+public class Admin extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ private String email;
+
+ @OneToOne(fetch = FetchType.LAZY)
+ @JoinColumn(nullable = false, unique = true)
+ private Building building;
+}
diff --git a/src/main/java/com/livable/server/entity/BaseTimeEntity.java b/src/main/java/com/livable/server/entity/BaseTimeEntity.java
new file mode 100644
index 00000000..72f35efb
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/BaseTimeEntity.java
@@ -0,0 +1,23 @@
+package com.livable.server.entity;
+
+
+import lombok.Getter;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.EntityListeners;
+import javax.persistence.MappedSuperclass;
+import java.time.LocalDateTime;
+
+@Getter
+@MappedSuperclass
+@EntityListeners(AuditingEntityListener.class)
+public abstract class BaseTimeEntity {
+
+ @CreatedDate
+ private LocalDateTime createdAt;
+
+ @LastModifiedDate
+ private LocalDateTime updatedAt;
+}
diff --git a/src/main/java/com/livable/server/entity/Building.java b/src/main/java/com/livable/server/entity/Building.java
new file mode 100644
index 00000000..be0eafaa
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Building.java
@@ -0,0 +1,51 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.LocalTime;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class Building extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false)
+ private Boolean hasCafeteria;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Column(nullable = false)
+ private String address;
+
+ @Column(nullable = false)
+ private LocalTime startTime;
+
+ @Column(nullable = false)
+ private LocalTime endTime;
+
+ @Column(nullable = false)
+ private String scale;
+
+ @Column(nullable = false)
+ private String longitude;
+
+ @Column(nullable = false)
+ private String latitude;
+
+ @Column(nullable = false)
+ private String representativeImageUrl;
+
+ @Column(nullable = false)
+ private String parkingCostInformation;
+
+ @Column
+ private String subwayStation;
+}
diff --git a/src/main/java/com/livable/server/entity/BuildingRestaurantMap.java b/src/main/java/com/livable/server/entity/BuildingRestaurantMap.java
new file mode 100644
index 00000000..c0c5c41c
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/BuildingRestaurantMap.java
@@ -0,0 +1,39 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Table(
+ name = "building_restaurant_map",
+ uniqueConstraints =
+ @UniqueConstraint(
+ name = "BUILDING_RESTAURANT_UNIQUE_IDX",
+ columnNames = {"building_id", "restaurant_id"}
+ )
+)
+@Entity
+public class BuildingRestaurantMap extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false, name = "building_id")
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Building building;
+
+ @JoinColumn(nullable = false, name = "restaurant_id")
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Restaurant restaurant;
+
+ @Column(nullable = false)
+ private Boolean inBuilding;
+
+ @Column(nullable = false)
+ private Integer distance;
+}
diff --git a/src/main/java/com/livable/server/entity/CafeteriaReview.java b/src/main/java/com/livable/server/entity/CafeteriaReview.java
new file mode 100644
index 00000000..8333eb18
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/CafeteriaReview.java
@@ -0,0 +1,23 @@
+package com.livable.server.entity;
+
+import lombok.*;
+import lombok.experimental.SuperBuilder;
+
+import javax.persistence.*;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@SuperBuilder
+@Entity
+@ToString(callSuper = true)
+public class CafeteriaReview extends Review {
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private Evaluation taste;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Building building;
+}
diff --git a/src/main/java/com/livable/server/entity/CommonPlace.java b/src/main/java/com/livable/server/entity/CommonPlace.java
new file mode 100644
index 00000000..5ee2aa00
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/CommonPlace.java
@@ -0,0 +1,30 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class CommonPlace extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Building building;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Column(nullable = false)
+ private String floor;
+
+ @Column(nullable = false)
+ private String roomNumber;
+}
diff --git a/src/main/java/com/livable/server/entity/Company.java b/src/main/java/com/livable/server/entity/Company.java
new file mode 100644
index 00000000..02317c5f
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Company.java
@@ -0,0 +1,24 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class Company extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Building building;
+
+ @Column(nullable = false)
+ private String name;
+}
diff --git a/src/main/java/com/livable/server/entity/Evaluation.java b/src/main/java/com/livable/server/entity/Evaluation.java
new file mode 100644
index 00000000..fdbdfec2
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Evaluation.java
@@ -0,0 +1,7 @@
+package com.livable.server.entity;
+
+public enum Evaluation {
+
+ GOOD,
+ BAD;
+}
diff --git a/src/main/java/com/livable/server/entity/Invitation.java b/src/main/java/com/livable/server/entity/Invitation.java
new file mode 100644
index 00000000..4ba52ddc
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Invitation.java
@@ -0,0 +1,56 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class Invitation extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Member member;
+
+ @Column
+ private String description;
+
+ @Column(nullable = false)
+ private String purpose;
+
+ @Column(nullable = false)
+ private String officeName;
+
+ @Column(nullable = false)
+ private LocalDate startDate;
+
+ @Column(nullable = false)
+ private LocalDate endDate;
+
+ @Column(nullable = false)
+ private LocalTime startTime;
+
+ @Column(nullable = false)
+ private LocalTime endTime;
+
+ public void updateDateTime(LocalDateTime startDateTime, LocalDateTime endDateTime) {
+ this.startDate = startDateTime.toLocalDate();
+ this.startTime = startDateTime.toLocalTime();
+ this.endDate = endDateTime.toLocalDate();
+ this.endTime = endDateTime.toLocalTime();
+ }
+
+ public void updateDescription(String description) {
+ this.description = description;
+ }
+}
diff --git a/src/main/java/com/livable/server/entity/InvitationReservationMap.java b/src/main/java/com/livable/server/entity/InvitationReservationMap.java
new file mode 100644
index 00000000..a3a29de5
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/InvitationReservationMap.java
@@ -0,0 +1,25 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class InvitationReservationMap extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Invitation invitation;
+
+ @JoinColumn(nullable = false, unique = true)
+ @OneToOne(fetch = FetchType.LAZY)
+ private Reservation reservation;
+}
diff --git a/src/main/java/com/livable/server/entity/LunchBoxReview.java b/src/main/java/com/livable/server/entity/LunchBoxReview.java
new file mode 100644
index 00000000..a04f7bc8
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/LunchBoxReview.java
@@ -0,0 +1,15 @@
+package com.livable.server.entity;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+import javax.persistence.Entity;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@SuperBuilder
+@Entity
+public class LunchBoxReview extends Review {
+}
diff --git a/src/main/java/com/livable/server/entity/Member.java b/src/main/java/com/livable/server/entity/Member.java
new file mode 100644
index 00000000..32511131
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Member.java
@@ -0,0 +1,46 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class Member extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Company company;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private Role role;
+
+ @Column(nullable = false, unique = true)
+ private String email;
+
+ @Column(nullable = false)
+ private String password;
+
+ @Column(nullable = false)
+ private String profileImageUrl;
+
+ @Column
+ private String businessCardImageUrl;
+
+ @Column(nullable = false, unique = true)
+ private String contact;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Column(nullable = false, unique = true)
+ private String employeeNumber;
+}
diff --git a/src/main/java/com/livable/server/entity/Menu.java b/src/main/java/com/livable/server/entity/Menu.java
new file mode 100644
index 00000000..baa1c61a
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Menu.java
@@ -0,0 +1,27 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class Menu extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private MenuCategory menuCategory;
+
+ @Column(nullable = false, unique = true)
+ private String name;
+
+ @Column(nullable = false, length = 1000)
+ private String representativeImageUrl;
+}
diff --git a/src/main/java/com/livable/server/entity/MenuCategory.java b/src/main/java/com/livable/server/entity/MenuCategory.java
new file mode 100644
index 00000000..ca4fd9ea
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/MenuCategory.java
@@ -0,0 +1,20 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class MenuCategory extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Column(nullable = false, unique = true)
+ private String name;
+}
diff --git a/src/main/java/com/livable/server/entity/MenuChoiceLog.java b/src/main/java/com/livable/server/entity/MenuChoiceLog.java
new file mode 100644
index 00000000..19ad9dc3
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/MenuChoiceLog.java
@@ -0,0 +1,45 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.LocalDate;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Table(
+ name = "menu_choice_log",
+ uniqueConstraints =
+ @UniqueConstraint(
+ name = "MEMBER_DATE_UNIQUE_IDX",
+ columnNames = {"member_id", "date"}
+ )
+)
+@Entity
+public class MenuChoiceLog extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false, name = "member_id")
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Member member;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Building building;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Menu menu;
+
+ @Column(nullable = false, name = "date")
+ private LocalDate date;
+
+ public void updateMenu(Menu changedMenu) {
+ this.menu = changedMenu;
+ }
+}
diff --git a/src/main/java/com/livable/server/entity/MenuChoiceResult.java b/src/main/java/com/livable/server/entity/MenuChoiceResult.java
new file mode 100644
index 00000000..eed7411a
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/MenuChoiceResult.java
@@ -0,0 +1,40 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.LocalDate;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Table(
+ name = "menu_choice_result",
+ uniqueConstraints =
+ @UniqueConstraint(
+ name = "MEMBER_BUILDING_DATE_UNIQUE_IDX",
+ columnNames = {"building_id", "menu_id", "date"}
+ )
+)
+@Entity
+public class MenuChoiceResult extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false, name = "building_id")
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Building building;
+
+ @JoinColumn(nullable = false, name = "menu_id")
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Menu menu;
+
+ @Column(nullable = false, name = "date")
+ private LocalDate date;
+
+ @Column(nullable = false)
+ private Integer count;
+}
diff --git a/src/main/java/com/livable/server/entity/MenuChoiceWeeklyResult.java b/src/main/java/com/livable/server/entity/MenuChoiceWeeklyResult.java
new file mode 100644
index 00000000..33376fe2
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/MenuChoiceWeeklyResult.java
@@ -0,0 +1,52 @@
+package com.livable.server.entity;
+
+import java.time.LocalDate;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.Table;
+import javax.persistence.UniqueConstraint;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Table(
+ name = "menu_choice_weekly_result",
+ uniqueConstraints =
+ @UniqueConstraint(
+ name = "MENU_BUILDING_DATE_UNIQUE_IDX",
+ columnNames = {"building_id", "menu_id", "date"}
+ )
+)
+@Entity
+public class MenuChoiceWeeklyResult extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false, name = "building_id")
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Building building;
+
+ @JoinColumn(nullable = false, name = "menu_id")
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Menu menu;
+
+ @Column(nullable = false, name = "date")
+ private LocalDate date;
+
+ @Column(nullable = false)
+ private Integer count;
+}
diff --git a/src/main/java/com/livable/server/entity/Office.java b/src/main/java/com/livable/server/entity/Office.java
new file mode 100644
index 00000000..0dd955a6
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Office.java
@@ -0,0 +1,30 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class Office extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Company company;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Column(nullable = false)
+ private String floor;
+
+ @Column(nullable = false)
+ private String roomNumber;
+}
diff --git a/src/main/java/com/livable/server/entity/ParkingLog.java b/src/main/java/com/livable/server/entity/ParkingLog.java
new file mode 100644
index 00000000..efa86134
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/ParkingLog.java
@@ -0,0 +1,41 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class ParkingLog extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false, unique = true)
+ @OneToOne(fetch = FetchType.LAZY)
+ private Visitor visitor;
+
+ @Column(nullable = false)
+ private String carNumber;
+
+ @Column
+ private LocalDateTime inTime;
+
+ @Column
+ private LocalDateTime outTime;
+
+ @Column
+ private Integer stayTime;
+
+ public static ParkingLog create(Visitor visitor, String carNumber) {
+ return ParkingLog.builder()
+ .carNumber(carNumber)
+ .visitor(visitor)
+ .build();
+ }
+}
diff --git a/src/main/java/com/livable/server/entity/Point.java b/src/main/java/com/livable/server/entity/Point.java
new file mode 100644
index 00000000..230aac55
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Point.java
@@ -0,0 +1,28 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class Point extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false, unique = true)
+ @OneToOne(fetch = FetchType.LAZY)
+ private Member member;
+
+ @Column(nullable = false)
+ private Integer balance;
+
+ public void plusPoint(Integer amount) {
+ this.balance += amount;
+ }
+}
diff --git a/src/main/java/com/livable/server/entity/PointCode.java b/src/main/java/com/livable/server/entity/PointCode.java
new file mode 100644
index 00000000..3d9518f7
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/PointCode.java
@@ -0,0 +1,26 @@
+package com.livable.server.entity;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.List;
+
+@Getter
+@AllArgsConstructor
+public enum PointCode {
+
+ PA00("외부 식당 오점완 리뷰 작성을 통해 포인트 획득"),
+ PA01("구내 식당 오점완 리뷰 작성을 통해 포인트 획득"),
+ PA02("도시락 오점완 리뷰 작성을 통해 포인트 획득"),
+ PA03("오점완 7일차 달성 보상"),
+ PA04("오점완 14일차 달성 보상"),
+ PA05("오점완 21일차 달성 보상"),
+ PA06("오점완 28일차 달성 보상"),
+ PM00("제휴 카페 메뉴 할인에 대한 포인트 사용");
+
+ private final String description;
+
+ public static List getReviewPointCodes() {
+ return List.of(PA00, PA01, PA02);
+ }
+}
diff --git a/src/main/java/com/livable/server/entity/PointLog.java b/src/main/java/com/livable/server/entity/PointLog.java
new file mode 100644
index 00000000..cff5c029
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/PointLog.java
@@ -0,0 +1,49 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class PointLog extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Point point;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Review review;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private PointCode code;
+
+ @Column(nullable = false)
+ private Integer amount;
+
+ @Column(nullable = false)
+ private LocalDateTime paidAt;
+
+ @PrePersist
+ private void prePersist() {
+ paidAt = LocalDateTime.now();
+ }
+
+ public boolean isPaid(LocalDate date) {
+ LocalDateTime paidDateTime = this.getPaidAt();
+ LocalDate paidDate = LocalDate.of(paidDateTime.getYear(), paidDateTime.getMonth(), paidDateTime.getDayOfMonth());
+
+ return paidDate.equals(date);
+ }
+}
diff --git a/src/main/java/com/livable/server/entity/Reservation.java b/src/main/java/com/livable/server/entity/Reservation.java
new file mode 100644
index 00000000..ec23e5d5
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Reservation.java
@@ -0,0 +1,41 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.LocalDate;
+import java.time.LocalTime;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Table(
+ name = "reservation",
+ uniqueConstraints =
+ @UniqueConstraint(
+ name = "COMMON_PLACE_DATE_TIME_UNIQUE_IDX",
+ columnNames = {"common_place_id", "date", "time"}
+ )
+)
+@Entity
+public class Reservation extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Company company;
+
+ @JoinColumn(nullable = false, name = "common_place_id")
+ @ManyToOne(fetch = FetchType.LAZY)
+ private CommonPlace commonPlace;
+
+ @Column(nullable = false, name = "date")
+ private LocalDate date;
+
+ @Column(nullable = false, name = "time")
+ private LocalTime time;
+}
diff --git a/src/main/java/com/livable/server/entity/Restaurant.java b/src/main/java/com/livable/server/entity/Restaurant.java
new file mode 100644
index 00000000..368d0d11
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Restaurant.java
@@ -0,0 +1,69 @@
+package com.livable.server.entity;
+
+import com.livable.server.restaurant.dto.RestaurantByMenuProjection;
+import javax.persistence.Column;
+import javax.persistence.ColumnResult;
+import javax.persistence.ConstructorResult;
+import javax.persistence.Entity;
+import javax.persistence.EnumType;
+import javax.persistence.Enumerated;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.SqlResultSetMapping;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@SqlResultSetMapping(
+ name ="RestaurantsByMenuMapping",
+ classes = @ConstructorResult(
+ targetClass = RestaurantByMenuProjection.class,
+ columns = {
+ @ColumnResult(name = "restaurantId", type = Long.class),
+ @ColumnResult(name = "restaurantName", type = String.class),
+ @ColumnResult(name = "restaurantThumbnailUrl", type = String.class),
+ @ColumnResult(name = "address", type = String.class),
+ @ColumnResult(name = "inBuilding", type = Boolean.class),
+ @ColumnResult(name = "distance", type = Integer.class),
+ @ColumnResult(name = "review", type = String.class),
+ @ColumnResult(name = "tastePercentage", type = Integer.class),
+ }
+ )
+)
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class Restaurant extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private RestaurantCategory restaurantCategory;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Column(nullable = false)
+ private String contact;
+
+ @Column(nullable = false)
+ private String address;
+
+ @Column(nullable = false)
+ private String restaurantUrl;
+
+ @Column(nullable = false)
+ private String thumbnailImageUrl;
+
+ @Column(nullable = false)
+ private String representativeCategory;
+}
diff --git a/src/main/java/com/livable/server/entity/RestaurantCategory.java b/src/main/java/com/livable/server/entity/RestaurantCategory.java
new file mode 100644
index 00000000..fb3e1144
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/RestaurantCategory.java
@@ -0,0 +1,26 @@
+package com.livable.server.entity;
+
+import com.livable.server.core.exception.GlobalRuntimeException;
+import com.livable.server.restaurant.domain.RestaurantErrorCode;
+
+import java.util.Arrays;
+import java.util.List;
+
+public enum RestaurantCategory {
+
+ RESTAURANT(List.of("RESTAURANT", "restaurant", "Restaurant")),
+ CAFE(List.of("CAFE", "cafe", "Cafe"));
+
+ private final List symbols;
+
+ RestaurantCategory(List symbols) {
+ this.symbols = symbols;
+ }
+
+ public static RestaurantCategory of(String symbol) {
+ return Arrays.stream(values())
+ .filter(restaurantCategory -> restaurantCategory.symbols.contains(symbol))
+ .findFirst()
+ .orElseThrow(() -> new GlobalRuntimeException(RestaurantErrorCode.NOT_FOUND_CATEGORY));
+ }
+}
diff --git a/src/main/java/com/livable/server/entity/RestaurantMenuMap.java b/src/main/java/com/livable/server/entity/RestaurantMenuMap.java
new file mode 100644
index 00000000..ac70ae5a
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/RestaurantMenuMap.java
@@ -0,0 +1,33 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Table(
+ name = "restaurant_menu_map",
+ uniqueConstraints =
+ @UniqueConstraint(
+ name = "RESTAURANT_MENU_UNIQUE_IDX",
+ columnNames = {"restaurant_id", "menu_id"}
+ )
+)
+@Entity
+public class RestaurantMenuMap extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false, name = "restaurant_id")
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Restaurant restaurant;
+
+ @JoinColumn(nullable = false, name = "menu_id")
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Menu menu;
+}
diff --git a/src/main/java/com/livable/server/entity/RestaurantReview.java b/src/main/java/com/livable/server/entity/RestaurantReview.java
new file mode 100644
index 00000000..40a968bb
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/RestaurantReview.java
@@ -0,0 +1,37 @@
+package com.livable.server.entity;
+
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.experimental.SuperBuilder;
+
+import javax.persistence.*;
+
+@Getter
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@SuperBuilder
+@Entity
+public class RestaurantReview extends Review {
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Restaurant restaurant;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private Evaluation taste;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private Evaluation speed;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private Evaluation amount;
+
+ @Enumerated(EnumType.STRING)
+ @Column(nullable = false)
+ private Evaluation service;
+}
diff --git a/src/main/java/com/livable/server/entity/Review.java b/src/main/java/com/livable/server/entity/Review.java
new file mode 100644
index 00000000..789b1175
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Review.java
@@ -0,0 +1,75 @@
+package com.livable.server.entity;
+
+import com.livable.server.review.dto.Projection;
+import com.livable.server.review.dto.ReviewResponse;
+import lombok.*;
+import lombok.experimental.SuperBuilder;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+
+import javax.persistence.*;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@SqlResultSetMapping(
+ name = "ReviewAllList",
+ classes = @ConstructorResult(
+ targetClass = ReviewResponse.CalendarListDTO.class,
+ columns = {
+ @ColumnResult(name = "reviewId", type = Long.class),
+ @ColumnResult(name = "type", type = String.class),
+ @ColumnResult(name = "reviewImageUrl", type = String.class),
+ @ColumnResult(name = "reviewDate", type = LocalDate.class)
+ }
+ )
+)
+@SqlResultSetMapping(
+ name = "AllReviewDetailListMapping",
+ classes = @ConstructorResult(
+ targetClass = Projection.AllReviewDetailDTO.class,
+ columns = {
+ @ColumnResult(name = "reviewId", type = Long.class),
+ @ColumnResult(name = "reviewTitle", type = String.class),
+ @ColumnResult(name = "reviewTaste", type = String.class),
+ @ColumnResult(name = "reviewDescription", type = String.class),
+ @ColumnResult(name = "reviewCreatedAt", type = String.class),
+ @ColumnResult(name = "location", type = String.class),
+ @ColumnResult(name = "images", type = String.class),
+ @ColumnResult(name = "reviewType", type = String.class)
+ }
+ )
+)
+@Getter
+@SuperBuilder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Inheritance(strategy = InheritanceType.JOINED)
+@EntityListeners(AuditingEntityListener.class)
+@Entity
+@ToString
+public class Review {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Member member;
+
+ @Column(nullable = false)
+ private String description;
+
+ @Column(nullable = false)
+ private String selectedDishes;
+
+ @CreatedDate
+ @Column(nullable = false)
+ private LocalDateTime createdAt;
+
+ @LastModifiedDate
+ @Column(nullable = false)
+ private LocalDateTime updatedAt;
+}
+
diff --git a/src/main/java/com/livable/server/entity/ReviewImage.java b/src/main/java/com/livable/server/entity/ReviewImage.java
new file mode 100644
index 00000000..9fd5c499
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/ReviewImage.java
@@ -0,0 +1,46 @@
+package com.livable.server.entity;
+
+import com.livable.server.review.dto.RestaurantReviewProjection;
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+
+@SqlResultSetMapping(
+ name = "RestaurantReviewListMapping",
+ classes = @ConstructorResult(
+ targetClass = RestaurantReviewProjection.class,
+ columns = {
+ @ColumnResult(name = "memberName", type = String.class),
+ @ColumnResult(name = "memberProfileImage", type = String.class),
+ @ColumnResult(name = "restaurantId", type = Long.class),
+ @ColumnResult(name = "restaurantName", type = String.class),
+ @ColumnResult(name = "reviewId", type = Long.class),
+ @ColumnResult(name = "reviewCreatedAt", type = LocalDateTime.class),
+ @ColumnResult(name = "reviewDescription", type = String.class),
+ @ColumnResult(name = "reviewTaste", type = String.class),
+ @ColumnResult(name = "reviewAmount", type = String.class),
+ @ColumnResult(name = "reviewService", type = String.class),
+ @ColumnResult(name = "reviewSpeed", type = String.class),
+ @ColumnResult(name = "images", type = String.class),
+ }
+ )
+)
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class ReviewImage extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Review review;
+
+ @Column(nullable = false)
+ private String url;
+}
diff --git a/src/main/java/com/livable/server/entity/ReviewMenuMap.java b/src/main/java/com/livable/server/entity/ReviewMenuMap.java
new file mode 100644
index 00000000..df3fbf09
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/ReviewMenuMap.java
@@ -0,0 +1,33 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Table(
+ name = "review_menu_map",
+ uniqueConstraints =
+ @UniqueConstraint(
+ name = "REVIEW_MENU_UNIQUE_IDX",
+ columnNames = {"review_Id", "menu_id"}
+ )
+)
+@Entity
+public class ReviewMenuMap extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Review review;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Menu menu;
+}
diff --git a/src/main/java/com/livable/server/entity/Role.java b/src/main/java/com/livable/server/entity/Role.java
new file mode 100644
index 00000000..307a887f
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Role.java
@@ -0,0 +1,6 @@
+package com.livable.server.entity;
+
+public enum Role {
+ ADMIN,
+ USER;
+}
diff --git a/src/main/java/com/livable/server/entity/Visitor.java b/src/main/java/com/livable/server/entity/Visitor.java
new file mode 100644
index 00000000..4f0f0661
--- /dev/null
+++ b/src/main/java/com/livable/server/entity/Visitor.java
@@ -0,0 +1,41 @@
+package com.livable.server.entity;
+
+import lombok.*;
+
+import javax.persistence.*;
+import java.time.LocalDateTime;
+
+@Getter
+@Builder
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+@AllArgsConstructor(access = AccessLevel.PROTECTED)
+@Entity
+public class Visitor extends BaseTimeEntity {
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long id;
+
+ @JoinColumn(nullable = false)
+ @ManyToOne(fetch = FetchType.LAZY)
+ private Invitation invitation;
+
+ @Column(nullable = false)
+ private String name;
+
+ @Column(nullable = false)
+ private String contact;
+
+ @Column
+ private LocalDateTime firstVisitedTime;
+
+ public void entrance() {
+ if (isFirstEntrance()) {
+ this.firstVisitedTime = LocalDateTime.now();
+ }
+ }
+
+ private boolean isFirstEntrance() {
+ return this.firstVisitedTime == null;
+ }
+}
diff --git a/src/main/java/com/livable/server/home/controller/HomeController.java b/src/main/java/com/livable/server/home/controller/HomeController.java
new file mode 100644
index 00000000..df6c965c
--- /dev/null
+++ b/src/main/java/com/livable/server/home/controller/HomeController.java
@@ -0,0 +1,46 @@
+package com.livable.server.home.controller;
+
+import com.livable.server.core.response.ApiResponse;
+import com.livable.server.core.util.Actor;
+import com.livable.server.core.util.JwtTokenProvider;
+import com.livable.server.core.util.LoginActor;
+import com.livable.server.home.dto.HomeResponse.AccessCardDto;
+import com.livable.server.home.dto.HomeResponse.BuildingInfoDto;
+import com.livable.server.member.service.MemberService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RequiredArgsConstructor
+@RestController
+public class HomeController {
+
+ private final MemberService memberService;
+
+ @GetMapping("api/home")
+ public ResponseEntity> getHomeInfo(@LoginActor Actor actor) {
+
+ JwtTokenProvider.checkMemberToken(actor);
+
+ Long memberId = actor.getId();
+
+ BuildingInfoDto buildingInfoDto = memberService.getBuildingInfo(memberId);
+
+ return ApiResponse.success(buildingInfoDto, HttpStatus.OK);
+ }
+
+ @GetMapping("/api/access-card")
+ public ResponseEntity> getAccessCard(@LoginActor Actor actor){
+
+ JwtTokenProvider.checkMemberToken(actor);
+
+ Long memberId = actor.getId();
+
+ AccessCardDto accessCardDto = memberService.getAccessCardData(memberId);
+
+ return ApiResponse.success(accessCardDto, HttpStatus.OK);
+ }
+
+}
diff --git a/src/main/java/com/livable/server/home/dto/HomeResponse.java b/src/main/java/com/livable/server/home/dto/HomeResponse.java
new file mode 100644
index 00000000..e1d47aa9
--- /dev/null
+++ b/src/main/java/com/livable/server/home/dto/HomeResponse.java
@@ -0,0 +1,55 @@
+package com.livable.server.home.dto;
+
+import com.livable.server.member.dto.AccessCardProjection;
+import com.livable.server.member.dto.BuildingInfoProjection;
+import lombok.AccessLevel;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class HomeResponse {
+
+ @Getter
+ @Builder
+ public static class BuildingInfoDto {
+
+ private Long buildingId;
+ private String buildingName;
+ private Boolean hasCafeteria;
+
+ public static BuildingInfoDto from(BuildingInfoProjection buildingInfoProjection) {
+ return BuildingInfoDto.builder()
+ .buildingId(buildingInfoProjection.getBuildingId())
+ .buildingName(buildingInfoProjection.getBuildingName())
+ .hasCafeteria(buildingInfoProjection.getHasCafeteria())
+ .build();
+ }
+
+ }
+
+ @Getter
+ @Builder
+ public static class AccessCardDto {
+
+ private String buildingName;
+ private String employeeNumber;
+ private String companyName;
+ private String floor;
+ private String roomNumber;
+ private String employeeName;
+
+ public static AccessCardDto from(AccessCardProjection accessCardProjection) {
+ return AccessCardDto
+ .builder()
+ .buildingName(accessCardProjection.getBuildingName())
+ .employeeNumber(accessCardProjection.getEmployeeNumber())
+ .companyName(accessCardProjection.getCompanyName())
+ .floor(accessCardProjection.getFloor())
+ .roomNumber(accessCardProjection.getRoomNumber())
+ .employeeName(accessCardProjection.getEmployeeName())
+ .build();
+ }
+ }
+
+}
diff --git a/src/main/java/com/livable/server/invitation/controller/InvitationController.java b/src/main/java/com/livable/server/invitation/controller/InvitationController.java
new file mode 100644
index 00000000..ec43d010
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/controller/InvitationController.java
@@ -0,0 +1,92 @@
+package com.livable.server.invitation.controller;
+
+import com.livable.server.core.exception.GlobalRuntimeException;
+import com.livable.server.core.response.ApiResponse.Success;
+import com.livable.server.core.util.Actor;
+import com.livable.server.core.util.ActorType;
+import com.livable.server.core.util.JwtTokenProvider;
+import com.livable.server.core.util.LoginActor;
+import com.livable.server.invitation.dto.InvitationRequest;
+import com.livable.server.invitation.dto.InvitationResponse;
+import com.livable.server.invitation.service.InvitationService;
+import com.livable.server.member.domain.MemberErrorCode;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Map;
+
+@RequiredArgsConstructor
+@RequestMapping("/api/invitation")
+@RestController
+public class InvitationController {
+
+ private final InvitationService invitationService;
+
+ @GetMapping("/places/available")
+ public ResponseEntity> getAvailablePlaces(@LoginActor Actor actor) {
+
+ JwtTokenProvider.checkMemberToken(actor);
+
+ Long memberId = actor.getId();
+
+ return invitationService.getAvailablePlaces(memberId);
+ }
+
+ @PostMapping
+ public ResponseEntity> createInvitation(
+ @Valid @RequestBody InvitationRequest.CreateDTO dto, @LoginActor Actor actor) {
+
+ JwtTokenProvider.checkMemberToken(actor);
+
+ Long memberId = actor.getId();
+
+ return invitationService.createInvitation(dto, memberId);
+ }
+
+ @GetMapping
+ public ResponseEntity>>> getInvitations(@LoginActor Actor actor) {
+
+ JwtTokenProvider.checkMemberToken(actor);
+
+ Long memberId = actor.getId();
+
+ return invitationService.getInvitations(memberId);
+ }
+
+ @GetMapping("/{invitationId}")
+ public ResponseEntity> getInvitation(
+ @PathVariable Long invitationId, @LoginActor Actor actor) {
+
+ JwtTokenProvider.checkMemberToken(actor);
+
+ Long memberId = actor.getId();
+
+ return invitationService.getInvitation(invitationId, memberId);
+ }
+
+ @DeleteMapping("/{invitationId}")
+ public ResponseEntity> deleteInvitation(@PathVariable Long invitationId, @LoginActor Actor actor) {
+
+ JwtTokenProvider.checkMemberToken(actor);
+
+ Long memberId = actor.getId();
+
+ return invitationService.deleteInvitation(invitationId, memberId);
+ }
+
+ @PatchMapping("/{invitationId}")
+ public ResponseEntity> updateInvitation(
+ @PathVariable Long invitationId, @Valid @RequestBody InvitationRequest.UpdateDTO dto, @LoginActor Actor actor) {
+
+ JwtTokenProvider.checkMemberToken(actor);
+
+ Long memberId = actor.getId();
+
+ return invitationService.updateInvitation(invitationId, dto, memberId);
+ }
+
+}
diff --git a/src/main/java/com/livable/server/invitation/controller/InvitationValidationController.java b/src/main/java/com/livable/server/invitation/controller/InvitationValidationController.java
new file mode 100644
index 00000000..b004fa76
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/controller/InvitationValidationController.java
@@ -0,0 +1,32 @@
+package com.livable.server.invitation.controller;
+
+import com.livable.server.invitation.service.InvitationValidationService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import javax.servlet.http.HttpServletResponse;
+
+@RequiredArgsConstructor
+@Controller
+public class InvitationValidationController {
+
+ private static final String visitationPageUrl = "https://livable.vercel.app/invitation/view";
+
+ private final InvitationValidationService invitationValidationService;
+
+ @GetMapping("api/invitation/callback")
+ public String validateVisitor(@RequestParam String token, HttpServletResponse response) {
+
+ invitationValidationService.validateVisitor(token);
+
+ response.setHeader("Authorization", createBearerToken(token));
+
+ return "redirect:" + visitationPageUrl;
+ }
+
+ private String createBearerToken(String token) {
+ return "Bearer " + token;
+ }
+}
diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java b/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java
new file mode 100644
index 00000000..d58d39e4
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/domain/InvitationErrorCode.java
@@ -0,0 +1,36 @@
+package com.livable.server.invitation.domain;
+
+import com.livable.server.core.exception.ErrorCode;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public enum InvitationErrorCode implements ErrorCode {
+ MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "존재하지 않는 회원 정보입니다."),
+ INVITATION_NOT_EXIST(HttpStatus.BAD_REQUEST, "존재하지 않는 초대장 정보입니다."),
+ INVALID_INTERVIEW_MAXIMUM_NUMBER(HttpStatus.BAD_REQUEST, "면접 초대 가능 인원수는 1명입니다."),
+ INVALID_INVITATION_MAXIMUM_NUMBER(HttpStatus.BAD_REQUEST, "한 초대장에 등록할 수 있는 최대 방문자는 30명입니다."),
+ INVALID_DATE(HttpStatus.BAD_REQUEST, "종료 날짜가 시작 날짜보다 과거일 수 없습니다."),
+ INVALID_TIME(HttpStatus.BAD_REQUEST, "종료 시간이 시작 시간보다 과거일 수 없습니다."),
+ INVALID_TIME_UNIT(HttpStatus.BAD_REQUEST, "시간의 분 단위는 0분 또는 30분이어야 합니다."),
+ INVALID_RESERVATION_COUNT(HttpStatus.BAD_REQUEST, "해당 날짜 또는 시간에 예약된 장소 정보가 없습니다."),
+ INVALID_INVITATION_OWNER(HttpStatus.FORBIDDEN, "초대장을 작성한 회원만 접근이 가능합니다."),
+ INVALID_DELETE_DATE(HttpStatus.BAD_REQUEST, "초대장 삭제는 방문일 기준 전날까지만 가능합니다."),
+ CAN_NOT_CHANGED_COMMON_PLACE_OF_INVITATION(HttpStatus.BAD_REQUEST, "초대장의 예약된 장소는 변경할 수 없습니다.");
+
+ private final HttpStatus httpStatus;
+ private final String message;
+
+ @Override
+ public HttpStatus getHttpStatus() {
+ return httpStatus;
+ }
+
+ @Override
+ public String getMessage() {
+ return message;
+ }
+}
diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationPurpose.java b/src/main/java/com/livable/server/invitation/domain/InvitationPurpose.java
new file mode 100644
index 00000000..a5d2c227
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/domain/InvitationPurpose.java
@@ -0,0 +1,19 @@
+package com.livable.server.invitation.domain;
+
+import lombok.AccessLevel;
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
+public enum InvitationPurpose {
+
+ MEETING("meeting"),
+ INTERVIEW("interview"),
+ PERIOD_WORK("fixedTermWork"),
+ SEMINAR("seminar"),
+ AFTER_SERVICE("as"),
+ ETC("etc");
+
+ private final String value;
+}
diff --git a/src/main/java/com/livable/server/invitation/domain/InvitationValidationMessage.java b/src/main/java/com/livable/server/invitation/domain/InvitationValidationMessage.java
new file mode 100644
index 00000000..05d62115
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/domain/InvitationValidationMessage.java
@@ -0,0 +1,11 @@
+package com.livable.server.invitation.domain;
+
+public interface InvitationValidationMessage {
+ String REQUIRED_FUTURE_DATE = "선택된 시간이 현재보다 과거일 수 없습니다.";
+ String REQUIRED_VISITOR_COUNT = "방문자는 최소 1명 이상 30명 이하여야 합니다. (면접은 1명)";
+ String NOT_NULL = "값이 Null 일수 없습니다.";
+ String VISITOR_NAME_MIN_SIZE = "방문자 이름은 2글자 이상이어야 합니다.";
+ String VISITOR_NAME_FORMAT = "방문자 이름은 한글만 입력해야 합니다.";
+ String VISITOR_CONTACT_MIN_SIZE = "방문자 전화번호는 10글자 이상이어야 합니다.";
+ String VISITOR_CONTACT_FORMAT = "방문자 전화번호는 숫자만 입력해야 합니다.";
+}
diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationDetailTimeDto.java b/src/main/java/com/livable/server/invitation/dto/InvitationDetailTimeDto.java
new file mode 100644
index 00000000..dcc04fe3
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/dto/InvitationDetailTimeDto.java
@@ -0,0 +1,16 @@
+package com.livable.server.invitation.dto;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+public interface InvitationDetailTimeDto {
+
+ LocalDate getStartDate();
+
+ LocalDate getEndDate();
+
+ LocalTime getStartTime();
+
+ LocalTime getEndTime();
+}
diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java b/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java
new file mode 100644
index 00000000..c32dec98
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/dto/InvitationRequest.java
@@ -0,0 +1,140 @@
+package com.livable.server.invitation.dto;
+
+import com.livable.server.entity.Invitation;
+import com.livable.server.entity.Member;
+import com.livable.server.entity.Visitor;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import javax.validation.Valid;
+import javax.validation.constraints.FutureOrPresent;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import javax.validation.constraints.Size;
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static com.livable.server.invitation.domain.InvitationValidationMessage.*;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class InvitationRequest {
+
+ @Getter
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class CreateDTO {
+
+ @NotNull(message = NOT_NULL)
+ private String purpose;
+
+ private Long commonPlaceId;
+
+ @NotNull(message = NOT_NULL)
+ private String officeName;
+
+ private String description;
+
+ @NotNull(message = NOT_NULL)
+ @FutureOrPresent(message = REQUIRED_FUTURE_DATE)
+ private LocalDateTime startDate;
+
+ @NotNull(message = NOT_NULL)
+ @FutureOrPresent(message = REQUIRED_FUTURE_DATE)
+ private LocalDateTime endDate;
+
+ @Valid
+ @Size(min = 1, max = 30, message = REQUIRED_VISITOR_COUNT)
+ private List visitors;
+
+ public Invitation toEntity(Member member) {
+ return Invitation.builder()
+ .member(member)
+ .purpose(purpose)
+ .officeName(officeName)
+ .description(description)
+ .startDate(startDate.toLocalDate())
+ .endDate(endDate.toLocalDate())
+ .startTime(startDate.toLocalTime())
+ .endTime(endDate.toLocalTime())
+ .build();
+ }
+ }
+
+ @Getter
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class VisitorCreateDTO {
+
+ @Pattern(regexp = "^[가-힣]*$", message = VISITOR_NAME_FORMAT)
+ @Size(min = 2, message = VISITOR_NAME_MIN_SIZE)
+ @NotNull(message = NOT_NULL)
+ private String name;
+
+ @Pattern(regexp = "^[0-9]*$", message = VISITOR_CONTACT_FORMAT)
+ @Size(min = 10, message = VISITOR_CONTACT_MIN_SIZE)
+ @NotNull(message = NOT_NULL)
+ private String contact;
+
+ public Visitor toEntity(Invitation invitation) {
+ return Visitor.builder()
+ .invitation(invitation)
+ .name(name)
+ .contact(contact)
+ .firstVisitedTime(null)
+ .build();
+ }
+ }
+
+ @Getter
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class UpdateDTO {
+ private Long commonPlaceId;
+ private String description;
+
+ @NotNull(message = NOT_NULL)
+ @FutureOrPresent(message = REQUIRED_FUTURE_DATE)
+ private LocalDateTime startDate;
+
+ @NotNull(message = NOT_NULL)
+ @FutureOrPresent(message = REQUIRED_FUTURE_DATE)
+ private LocalDateTime endDate;
+
+ @Valid
+ @NotNull(message = NOT_NULL)
+ private List visitors;
+ }
+
+ @Getter
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ public static class VisitorForUpdateDTO {
+
+ @Pattern(regexp = "^[가-힣]*$", message = VISITOR_NAME_FORMAT)
+ @Size(min = 2, message = VISITOR_NAME_MIN_SIZE)
+ @NotNull(message = NOT_NULL)
+ private String name;
+
+ @Pattern(regexp = "^[0-9]*$", message = VISITOR_CONTACT_FORMAT)
+ @Size(min = 10, message = VISITOR_CONTACT_MIN_SIZE)
+ @NotNull(message = NOT_NULL)
+ private String contact;
+
+ public Visitor toEntity(Invitation invitation) {
+ return Visitor.builder()
+ .invitation(invitation)
+ .name(name)
+ .contact(contact)
+ .firstVisitedTime(null)
+ .build();
+ }
+ }
+
+}
diff --git a/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java b/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java
new file mode 100644
index 00000000..4d55184b
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/dto/InvitationResponse.java
@@ -0,0 +1,138 @@
+package com.livable.server.invitation.dto;
+
+import com.livable.server.entity.Office;
+import lombok.*;
+
+import java.time.LocalDate;
+import java.time.LocalTime;
+import java.util.List;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class InvitationResponse {
+
+
+ @Getter
+ @Builder
+ public static class AvailablePlacesDTO {
+ private List offices;
+ private List commonPlaces;
+ }
+
+ @Getter
+ @Builder
+ public static class OfficeDTO {
+ private String officeName;
+
+ public static OfficeDTO from(Office office) {
+ return new OfficeDTO(getFormattedPlaceName(
+ office.getName(),
+ office.getFloor(),
+ office.getRoomNumber()
+ ));
+ }
+ }
+
+ @Getter
+ @Builder
+ public static class CommonPlaceDTO {
+ private Long commonPlaceId;
+ private String commonPlaceName;
+
+ public static CommonPlaceDTO from(ReservationDTO reservationDTO) {
+ return new CommonPlaceDTO(
+ reservationDTO.getCommonPlaceId(),
+ getFormattedPlaceName(
+ reservationDTO.getCommonPlaceName(),
+ reservationDTO.getCommonPlaceFloor(),
+ reservationDTO.getCommonPlaceRoomNumber()
+ )
+ );
+ }
+ }
+
+ private static String getFormattedPlaceName(String name, String floor, String roomNumber) {
+ return String.format("%s (%s층 %s호)", name, floor, roomNumber);
+ }
+
+ @Getter
+ @Setter
+ public static class ReservationDTO {
+ private Long commonPlaceId;
+ private String commonPlaceFloor;
+ private String commonPlaceRoomNumber;
+ private String commonPlaceName;
+
+ public ReservationDTO(
+ Long commonPlaceId,
+ String commonPlaceFloor,
+ String commonPlaceRoomNumber,
+ String commonPlaceName
+ ) {
+ this.commonPlaceId = commonPlaceId;
+ this.commonPlaceFloor = commonPlaceFloor;
+ this.commonPlaceRoomNumber = commonPlaceRoomNumber;
+ this.commonPlaceName = commonPlaceName;
+ }
+ }
+
+ @Getter
+ @AllArgsConstructor
+ public static class ListDTO {
+ private Long invitationId;
+ private String visitorName;
+ private Long visitorCount;
+ private String purpose;
+ private String officeName;
+ private LocalDate startDate;
+ private LocalTime startTime;
+ private LocalTime endTime;
+ }
+
+ @Getter
+ @Builder
+ @AllArgsConstructor
+ public static class DetailDTO {
+ private Long commonPlaceId;
+ private String officeName;
+ private String purpose;
+ private String description;
+ private LocalDate startDate;
+ private LocalDate endDate;
+ private LocalTime startTime;
+ private LocalTime endTime;
+ private List visitors;
+
+ public DetailDTO(
+ Long commonPlaceId,
+ String officeName,
+ String purpose,
+ String description,
+ LocalDate startDate,
+ LocalDate endDate,
+ LocalTime startTime,
+ LocalTime endTime
+ ) {
+ this.commonPlaceId = commonPlaceId;
+ this.officeName = officeName;
+ this.purpose = purpose;
+ this.description = description;
+ this.startDate = startDate;
+ this.endDate = endDate;
+ this.startTime = startTime;
+ this.endTime = endTime;
+ }
+
+ public void setVisitors(List visitors) {
+ this.visitors = visitors;
+ }
+ }
+
+ @Getter
+ @AllArgsConstructor
+ public static class VisitorForDetailDTO {
+ private Long visitorId;
+ private String name;
+ private String contact;
+ }
+
+}
diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java
new file mode 100644
index 00000000..aed6fd79
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepository.java
@@ -0,0 +1,11 @@
+package com.livable.server.invitation.repository;
+
+import com.livable.server.invitation.dto.InvitationResponse;
+
+import java.util.List;
+
+public interface InvitationQueryRepository {
+ List findInvitationsByMemberId(Long memberId);
+ InvitationResponse.DetailDTO findInvitationAndVisitorsByInvitationId(Long invitationId);
+ Long getCommonPlaceIdByInvitationId(Long invitationId);
+}
diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java
new file mode 100644
index 00000000..77954fe3
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/repository/InvitationQueryRepositoryImpl.java
@@ -0,0 +1,107 @@
+package com.livable.server.invitation.repository;
+
+import com.livable.server.entity.QVisitor;
+import com.livable.server.invitation.dto.InvitationResponse;
+import com.querydsl.core.types.Projections;
+import com.querydsl.jpa.JPAExpressions;
+import com.querydsl.jpa.impl.JPAQueryFactory;
+import lombok.RequiredArgsConstructor;
+
+import java.time.LocalDate;
+import java.util.List;
+
+import static com.livable.server.entity.QInvitation.invitation;
+import static com.livable.server.entity.QInvitationReservationMap.invitationReservationMap;
+import static com.livable.server.entity.QReservation.reservation;
+import static com.livable.server.entity.QVisitor.visitor;
+
+@RequiredArgsConstructor
+public class InvitationQueryRepositoryImpl implements InvitationQueryRepository {
+
+ private final JPAQueryFactory queryFactory;
+
+ @Override
+ public List findInvitationsByMemberId(Long memberId) {
+ QVisitor subVisitorForMaxId = new QVisitor("subVisitor");
+ QVisitor subVisitor = new QVisitor("subVisitor");
+
+ return queryFactory
+ .select(Projections.constructor(InvitationResponse.ListDTO.class,
+ visitor.invitation.id,
+ JPAExpressions
+ .select(subVisitor.name)
+ .from(subVisitor)
+ .where(subVisitor.id.eq(
+ JPAExpressions
+ .select(subVisitorForMaxId.id.min())
+ .from(subVisitorForMaxId)
+ .where(subVisitorForMaxId.invitation.id.eq(visitor.invitation.id))
+ )),
+ visitor.invitation.id.count(),
+ invitation.purpose,
+ invitation.officeName,
+ invitation.startDate,
+ invitation.startTime,
+ invitation.endTime
+ ))
+ .from(visitor)
+ .innerJoin(invitation)
+ .on(
+ visitor.invitation.id.eq(invitation.id),
+ invitation.member.id.eq(memberId),
+ invitation.startDate.goe(LocalDate.now())
+ )
+ .groupBy(visitor.invitation.id)
+ .orderBy(invitation.startDate.asc(), invitation.startTime.asc())
+ .fetch();
+ }
+
+ @Override
+ public InvitationResponse.DetailDTO findInvitationAndVisitorsByInvitationId(Long invitationId) {
+ InvitationResponse.DetailDTO invitationDetail = queryFactory
+ .select(Projections.constructor(InvitationResponse.DetailDTO.class,
+ reservation.commonPlace.id,
+ invitation.officeName,
+ invitation.purpose,
+ invitation.description,
+ invitation.startDate,
+ invitation.endDate,
+ invitation.startTime,
+ invitation.endTime
+ ))
+ .from(invitation)
+ .leftJoin(invitationReservationMap)
+ .on(invitationReservationMap.invitation.id.eq(invitation.id))
+ .leftJoin(reservation)
+ .on(reservation.id.eq(invitationReservationMap.reservation.id))
+ .where(invitation.id.eq(invitationId))
+ .fetchFirst();
+
+ List visitors = queryFactory
+ .select(Projections.constructor(InvitationResponse.VisitorForDetailDTO.class,
+ visitor.id,
+ visitor.name,
+ visitor.contact
+ ))
+ .from(visitor)
+ .where(visitor.invitation.id.eq(invitationId))
+ .fetch();
+
+ invitationDetail.setVisitors(visitors);
+
+ return invitationDetail;
+ }
+
+ @Override
+ public Long getCommonPlaceIdByInvitationId(Long invitationId) {
+ return queryFactory
+ .select(reservation.commonPlace.id)
+ .from(invitation)
+ .leftJoin(invitationReservationMap)
+ .on(invitationReservationMap.invitation.id.eq(invitation.id))
+ .leftJoin(reservation)
+ .on(reservation.id.eq(invitationReservationMap.reservation.id))
+ .where(invitation.id.eq(invitationId))
+ .fetchFirst();
+ }
+}
diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java b/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java
new file mode 100644
index 00000000..ff3339af
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/repository/InvitationRepository.java
@@ -0,0 +1,22 @@
+package com.livable.server.invitation.repository;
+
+import com.livable.server.entity.Invitation;
+import com.livable.server.invitation.dto.InvitationDetailTimeDto;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+import java.util.Optional;
+
+public interface InvitationRepository extends JpaRepository, InvitationQueryRepository {
+
+ @Query("select i.startTime as startTime, i.endTime as endTime, i.startDate as startDate, i.endDate as endDate" +
+ " from Visitor v" +
+ " join fetch Invitation i" +
+ " on v.invitation = i" +
+ " where v.id = :visitorId")
+ Optional findInvitationDetailTimeByVisitorId(@Param("visitorId") Long visitorId);
+
+ @Query("select count(i) from Invitation i where i.id = :invitationId and i.member.id = :memberId")
+ Long countByIdAndMemberId(@Param("invitationId") Long invitationId, @Param("memberId") Long memberId);
+}
diff --git a/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java b/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java
new file mode 100644
index 00000000..4504ac11
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/repository/InvitationReservationMapRepository.java
@@ -0,0 +1,18 @@
+package com.livable.server.invitation.repository;
+
+import com.livable.server.entity.InvitationReservationMap;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+
+import java.util.List;
+
+public interface InvitationReservationMapRepository extends JpaRepository {
+
+ @Modifying
+ @Query("delete from InvitationReservationMap irm where irm.invitation.id = :invitationId")
+ void deleteAllByInvitationId(Long invitationId);
+
+ @Query("select ir.reservation.id from InvitationReservationMap ir")
+ List findAllReservationId();
+}
diff --git a/src/main/java/com/livable/server/invitation/repository/OfficeRepository.java b/src/main/java/com/livable/server/invitation/repository/OfficeRepository.java
new file mode 100644
index 00000000..ac4a8304
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/repository/OfficeRepository.java
@@ -0,0 +1,11 @@
+package com.livable.server.invitation.repository;
+
+import com.livable.server.entity.Office;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.List;
+
+public interface OfficeRepository extends JpaRepository {
+
+ List findAllByCompanyId(Long companyId);
+}
diff --git a/src/main/java/com/livable/server/invitation/service/InvitationService.java b/src/main/java/com/livable/server/invitation/service/InvitationService.java
new file mode 100644
index 00000000..510d603c
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/service/InvitationService.java
@@ -0,0 +1,373 @@
+package com.livable.server.invitation.service;
+
+import com.livable.server.core.exception.GlobalRuntimeException;
+import com.livable.server.core.response.ApiResponse;
+import com.livable.server.core.response.ApiResponse.Success;
+import com.livable.server.entity.*;
+import com.livable.server.invitation.domain.InvitationErrorCode;
+import com.livable.server.invitation.domain.InvitationPurpose;
+import com.livable.server.invitation.dto.InvitationDetailTimeDto;
+import com.livable.server.invitation.dto.InvitationRequest;
+import com.livable.server.invitation.dto.InvitationResponse;
+import com.livable.server.invitation.repository.InvitationRepository;
+import com.livable.server.invitation.repository.InvitationReservationMapRepository;
+import com.livable.server.invitation.repository.OfficeRepository;
+import com.livable.server.member.repository.MemberRepository;
+import com.livable.server.reservation.repository.ReservationRepository;
+import com.livable.server.visitation.domain.VisitationErrorCode;
+import com.livable.server.visitation.dto.VisitationResponse;
+import com.livable.server.visitation.repository.ParkingLogRepository;
+import com.livable.server.visitation.repository.VisitorRepository;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.Duration;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@RequiredArgsConstructor
+@Service
+public class InvitationService {
+ private static final int INTERVIEW_MAXIMUM_COUNT = 1;
+ private static final int INVITATION_MAXIMUM_COUNT = 30;
+
+ private final MemberRepository memberRepository;
+ private final OfficeRepository officeRepository;
+ private final InvitationRepository invitationRepository;
+ private final ReservationRepository reservationRepository;
+ private final InvitationReservationMapRepository invitationReservationMapRepository;
+ private final VisitorRepository visitorRepository;
+ private final ParkingLogRepository parkingLogRepository;
+
+ public VisitationResponse.InvitationTimeDto findInvitationTime(Long visitorId) {
+ InvitationDetailTimeDto invitationDetailTimeDto = invitationRepository.findInvitationDetailTimeByVisitorId(visitorId)
+ .orElseThrow(() -> new GlobalRuntimeException(VisitationErrorCode.NOT_FOUND));
+
+ return VisitationResponse.InvitationTimeDto.from(invitationDetailTimeDto);
+ }
+
+ @Transactional(readOnly = true)
+ public ResponseEntity> getAvailablePlaces(Long memberId) {
+ Long companyId = getCompanyIdByMemberId(memberId);
+
+ List officeEntities = officeRepository.findAllByCompanyId(companyId);
+
+ List reservations = reservationRepository
+ .findReservationsByCompanyId(companyId);
+
+ List offices = officeEntities.stream()
+ .map(InvitationResponse.OfficeDTO::from).collect(Collectors.toList());
+
+ List commonPlaces = reservations.stream()
+ .map(InvitationResponse.CommonPlaceDTO::from).collect(Collectors.toList());
+
+ InvitationResponse.AvailablePlacesDTO responseBody = InvitationResponse.AvailablePlacesDTO.builder()
+ .offices(offices)
+ .commonPlaces(commonPlaces)
+ .build();
+
+ return ApiResponse.success(responseBody, HttpStatus.OK);
+ }
+
+ private Long getCompanyIdByMemberId(Long memberId) {
+ Member member = checkExistMemberById(memberId);
+
+ return member.getCompany().getId();
+ }
+
+ @Transactional
+ public ResponseEntity> createInvitation(InvitationRequest.CreateDTO dto, Long memberId) {
+ checkInterviewVisitorCount(dto);
+
+ Member member = checkExistMemberById(memberId);
+ Invitation invitation = createInvitation(dto, member);
+ createVisitors(dto.getVisitors(), invitation);
+ reserveCommonPlaces(dto, invitation);
+
+ return ApiResponse.success(HttpStatus.CREATED);
+ }
+
+ private void checkInterviewVisitorCount(InvitationRequest.CreateDTO dto) {
+ if (dto.getPurpose().equals(InvitationPurpose.INTERVIEW.getValue())
+ && dto.getVisitors().size() > INTERVIEW_MAXIMUM_COUNT) {
+ throw new GlobalRuntimeException(InvitationErrorCode.INVALID_INTERVIEW_MAXIMUM_NUMBER);
+ }
+ }
+
+ private Member checkExistMemberById(Long memberId) {
+ Optional memberOptional = memberRepository.findById(memberId);
+
+ return memberOptional.orElseThrow(() -> new GlobalRuntimeException(InvitationErrorCode.MEMBER_NOT_EXIST));
+ }
+
+ private Invitation createInvitation(InvitationRequest.CreateDTO dto, Member member) {
+ Invitation invitation = dto.toEntity(member);
+
+ return invitationRepository.save(invitation);
+ }
+
+ private void createVisitors(List visitorCreateDTOS, Invitation invitation) {
+ for (InvitationRequest.VisitorCreateDTO visitorCreateDTO : visitorCreateDTOS) {
+ Visitor visitor = visitorCreateDTO.toEntity(invitation);
+ visitorRepository.save(visitor);
+ }
+ }
+
+ private void reserveCommonPlaces(InvitationRequest.CreateDTO dto, Invitation invitation) {
+ LocalDateTime startDateTime = dto.getStartDate();
+ LocalDateTime endDateTime = dto.getEndDate();
+ checkDateTimeValidate(startDateTime, endDateTime);
+
+ if (isReservedCommonPlace(dto.getCommonPlaceId())) {
+ int expectedReservationCount = getExpectedReservationCount(startDateTime, endDateTime);
+ List reservations = reservationRepository
+ .findReservationsByCommonPlaceIdAndStartDateAndEndDate(dto.getCommonPlaceId(), startDateTime, endDateTime);
+
+ checkReservationCount(reservations, expectedReservationCount);
+ createInvitationReservationMap(reservations, invitation);
+ }
+ }
+
+ private void checkDateTimeValidate(LocalDateTime startDateTime, LocalDateTime endDateTime) {
+ if (endDateTime.toLocalDate().isBefore(startDateTime.toLocalDate())) {
+ throw new GlobalRuntimeException(InvitationErrorCode.INVALID_DATE);
+ }
+ if (!endDateTime.toLocalTime().isAfter(startDateTime.toLocalTime())) {
+ throw new GlobalRuntimeException(InvitationErrorCode.INVALID_TIME);
+ }
+ checkTimeUnitValidation(startDateTime.toLocalTime(), endDateTime.toLocalTime());
+ }
+
+ private void checkTimeUnitValidation(LocalTime startTime, LocalTime endTime) {
+ int startMinute = startTime.getMinute();
+ int endMinute = endTime.getMinute();
+
+ if ((startMinute % 30 != 0) || (endMinute % 30 != 0)) {
+ throw new GlobalRuntimeException(InvitationErrorCode.INVALID_TIME_UNIT);
+ }
+ }
+
+ private boolean isReservedCommonPlace(Long commonPlaceId) {
+ return commonPlaceId != null;
+ }
+
+ /* 입력된 시간 범위 내에서 반드시 존재해야 하는 예약 정보 개수를 반환 */
+ private int getExpectedReservationCount(LocalDateTime startDateTime, LocalDateTime endDateTime) {
+ LocalDate startDate = startDateTime.toLocalDate();
+ LocalDate endDate = endDateTime.toLocalDate();
+ LocalTime startTime = startDateTime.toLocalTime();
+ LocalTime endTime = endDateTime.toLocalTime();
+
+ int dayCount = (int) Duration.between(startDate.atStartOfDay(), endDate.atStartOfDay()).toDays() + 1;
+ int timeCount = (endTime.toSecondOfDay() - startTime.toSecondOfDay()) / 1800;
+
+ return dayCount * timeCount;
+ }
+
+ /* 입력된 시작, 종료 날짜에 대한 예약 정보 개수가 예상한 값과 맞는지 확인 */
+ private void checkReservationCount(List reservations, int count) {
+ if (reservations.size() != count) {
+ throw new GlobalRuntimeException(InvitationErrorCode.INVALID_RESERVATION_COUNT);
+ }
+ }
+
+ private void createInvitationReservationMap(List reservations, Invitation invitation) {
+ for (Reservation reservation : reservations) {
+ InvitationReservationMap invitationReservationMap = InvitationReservationMap.builder()
+ .invitation(invitation)
+ .reservation(reservation)
+ .build();
+
+ invitationReservationMapRepository.save(invitationReservationMap);
+ }
+ }
+
+ @Transactional(readOnly = true)
+ public ResponseEntity>>> getInvitations(Long memberId) {
+ checkExistMemberById(memberId);
+ List invitationDTOs = invitationRepository.findInvitationsByMemberId(memberId);
+ Map> responseBody = getInvitationsGroupByLocalDate(invitationDTOs);
+
+ return ApiResponse.success(responseBody, HttpStatus.OK);
+ }
+
+ private Map> getInvitationsGroupByLocalDate(
+ List invitationDTOS) {
+
+ Map> invitationsGroupByLocalDate
+ = new TreeMap<>(LocalDate::compareTo);
+
+ for (InvitationResponse.ListDTO invitation : invitationDTOS) {
+ LocalDate date = invitation.getStartDate();
+
+ if (!invitationsGroupByLocalDate.containsKey(date)) {
+ invitationsGroupByLocalDate.put(date, new ArrayList<>());
+ }
+ invitationsGroupByLocalDate.get(date).add(invitation);
+ }
+
+ return invitationsGroupByLocalDate;
+ }
+
+ @Transactional(readOnly = true)
+ public ResponseEntity> getInvitation(Long invitationId, Long memberId) {
+ checkExistMemberById(memberId);
+ checkInvitationOwner(invitationId, memberId);
+
+ InvitationResponse.DetailDTO invitationDTO
+ = invitationRepository.findInvitationAndVisitorsByInvitationId(invitationId);
+
+ return ApiResponse.success(invitationDTO, HttpStatus.OK);
+ }
+
+ private void checkInvitationOwner(Long invitationId, Long memberId) {
+
+ Optional invitationOptional = invitationRepository.findById(invitationId);
+ Invitation invitation = invitationOptional
+ .orElseThrow(() -> new GlobalRuntimeException(InvitationErrorCode.INVITATION_NOT_EXIST));
+
+ if (!invitation.getMember().getId().equals(memberId)) {
+ throw new GlobalRuntimeException(InvitationErrorCode.INVALID_INVITATION_OWNER);
+ }
+ }
+
+ @Transactional
+ public ResponseEntity> deleteInvitation(Long invitationId, Long memberId) {
+ checkExistMemberById(memberId);
+ checkInvitationOwner(invitationId, memberId);
+
+ // Step 1. 초대장 가져옴
+ Optional invitationOptional = invitationRepository.findById(invitationId);
+ Invitation invitation = invitationOptional
+ .orElseThrow(() -> new GlobalRuntimeException(InvitationErrorCode.INVITATION_NOT_EXIST));
+
+ // Step 2. 초대장 방문날짜 확인
+ checkInvitationStartDate(invitation);
+
+ // Step 3. 예약된 장소에 대한 예약 정보 제거
+ deleteReservationsByInvitation(invitation);
+
+ // Step 4. 초대장에 등록된 방문자 데이터 + 주차 등록 데이터 삭제
+ deleteVisitorsAndParkingLogByInvitation(invitation);
+
+ // Step 5. 초대장 삭제
+ invitationRepository.delete(invitation);
+
+ return ApiResponse.success(HttpStatus.OK);
+ }
+
+ private void checkInvitationStartDate(Invitation invitation) {
+ if (invitation.getStartDate().isBefore(LocalDate.now())) {
+ throw new GlobalRuntimeException(InvitationErrorCode.INVALID_DELETE_DATE);
+ }
+ }
+
+ private void deleteReservationsByInvitation(Invitation invitation) {
+ invitationReservationMapRepository.deleteAllByInvitationId(invitation.getId());
+ }
+
+ private void deleteVisitorsAndParkingLogByInvitation(Invitation invitation) {
+ List visitors = visitorRepository.findVisitorsByInvitation(invitation);
+ List visitorsIds = visitors.stream().map(Visitor::getId).collect(Collectors.toList());
+
+ parkingLogRepository.deleteByVisitorIdsIn(visitorsIds);
+ visitorRepository.deleteByIdsIn(visitorsIds);
+ }
+
+ @Transactional
+ public ResponseEntity> updateInvitation(Long invitationId, InvitationRequest.UpdateDTO dto, Long memberId) {
+ checkExistMemberById(memberId);
+ checkInvitationOwner(invitationId, memberId);
+
+ Optional invitationOptional = invitationRepository.findById(invitationId);
+ Invitation invitation = invitationOptional
+ .orElseThrow(() -> new GlobalRuntimeException(InvitationErrorCode.INVITATION_NOT_EXIST));
+
+ checkInvitationStartDate(invitation);
+ checkModifiedCommonPlaceId(invitation, dto);
+
+ boolean shouldSendToAlreadyVisitor = false;
+ boolean shouldSendToAddedVisitor = checkAddedVisitorsCount(invitation, dto);
+
+ if (isModifiedInvitationDateTime(invitation, dto)) {
+ shouldSendToAlreadyVisitor = true;
+ if (isReservedCommonPlace(dto.getCommonPlaceId())) {
+
+ invitationReservationMapRepository.deleteAllByInvitationId(invitation.getId());
+ reserveNewCommonPlaces(dto, invitation);
+ }
+ }
+
+ invitation.updateDateTime(dto.getStartDate(), dto.getEndDate());
+ invitation.updateDescription(dto.getDescription());
+
+ if (shouldSendToAlreadyVisitor) {
+ List currentVisitors = visitorRepository.findVisitorsByInvitation(invitation);
+
+ // TODO: 기존 등록되어 있던 방문자들에게 알림톡을 다시 보내는 로직 추가
+ }
+
+ if (shouldSendToAddedVisitor) {
+ List visitors = dto.getVisitors().stream()
+ .map(visitor -> visitor.toEntity(invitation)).collect(Collectors.toList());
+
+ visitorRepository.saveAll(visitors);
+
+ // TODO: 새로 등록된 방문자들에게 알림톡을 다시 보내는 로직 추가
+ }
+
+ return ApiResponse.success(HttpStatus.OK);
+ }
+
+ private boolean checkAddedVisitorsCount(Invitation invitation, InvitationRequest.UpdateDTO dto) {
+ long addedCount = dto.getVisitors().size();
+
+ if (addedCount != 0L && invitation.getPurpose().equals(InvitationPurpose.INTERVIEW.getValue())) {
+ throw new GlobalRuntimeException(InvitationErrorCode.INVALID_INTERVIEW_MAXIMUM_NUMBER);
+ }
+
+ long alreadyCount = visitorRepository.countByInvitation(invitation);
+
+ if (alreadyCount + addedCount > INVITATION_MAXIMUM_COUNT) {
+ throw new GlobalRuntimeException(InvitationErrorCode.INVALID_INVITATION_MAXIMUM_NUMBER);
+ }
+
+ return addedCount != 0;
+ }
+
+ private boolean isModifiedInvitationDateTime(Invitation invitation, InvitationRequest.UpdateDTO dto) {
+ return !LocalDateTime.of(invitation.getStartDate(), invitation.getStartTime()).isEqual(dto.getStartDate())
+ || !LocalDateTime.of(invitation.getEndDate(), invitation.getEndTime()).isEqual(dto.getEndDate());
+ }
+
+ private void checkModifiedCommonPlaceId(Invitation invitation, InvitationRequest.UpdateDTO dto) {
+ Long currentCommonPlaceId = invitationRepository.getCommonPlaceIdByInvitationId(invitation.getId());
+
+ if (currentCommonPlaceId != null && !currentCommonPlaceId.equals(dto.getCommonPlaceId())) {
+ throw new GlobalRuntimeException(InvitationErrorCode.CAN_NOT_CHANGED_COMMON_PLACE_OF_INVITATION);
+ }
+
+ if (currentCommonPlaceId == null && dto.getCommonPlaceId() != null) {
+ throw new GlobalRuntimeException(InvitationErrorCode.CAN_NOT_CHANGED_COMMON_PLACE_OF_INVITATION);
+ }
+ }
+
+ private void reserveNewCommonPlaces(InvitationRequest.UpdateDTO dto, Invitation invitation) {
+ LocalDateTime startDateTime = dto.getStartDate();
+ LocalDateTime endDateTime = dto.getEndDate();
+ checkDateTimeValidate(startDateTime, endDateTime);
+
+ int expectedReservationCount = getExpectedReservationCount(startDateTime, endDateTime);
+ List reservations = reservationRepository
+ .findReservationsByCommonPlaceIdAndStartDateAndEndDate(dto.getCommonPlaceId(), startDateTime, endDateTime);
+
+ checkReservationCount(reservations, expectedReservationCount);
+ createInvitationReservationMap(reservations, invitation);
+ }
+}
diff --git a/src/main/java/com/livable/server/invitation/service/InvitationValidationService.java b/src/main/java/com/livable/server/invitation/service/InvitationValidationService.java
new file mode 100644
index 00000000..cf176e00
--- /dev/null
+++ b/src/main/java/com/livable/server/invitation/service/InvitationValidationService.java
@@ -0,0 +1,25 @@
+package com.livable.server.invitation.service;
+
+import com.livable.server.core.exception.GlobalRuntimeException;
+import com.livable.server.core.util.ActorType;
+import com.livable.server.core.util.JwtTokenProvider;
+import com.livable.server.member.domain.MemberErrorCode;
+import io.jsonwebtoken.Claims;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@RequiredArgsConstructor
+@Service
+public class InvitationValidationService {
+
+ private final JwtTokenProvider tokenProvider;
+
+ public void validateVisitor(String token) {
+ Claims claims = tokenProvider.parseClaims(token);
+
+ String actorType = claims.get("actorType", String.class);
+ if (!actorType.equals(ActorType.VISITOR.name())) {
+ throw new GlobalRuntimeException(MemberErrorCode.INVALID_TOKEN);
+ }
+ }
+}
diff --git a/src/main/java/com/livable/server/member/controller/MemberController.java b/src/main/java/com/livable/server/member/controller/MemberController.java
new file mode 100644
index 00000000..74769bd3
--- /dev/null
+++ b/src/main/java/com/livable/server/member/controller/MemberController.java
@@ -0,0 +1,31 @@
+package com.livable.server.member.controller;
+
+import com.livable.server.core.response.ApiResponse;
+import com.livable.server.core.util.Actor;
+import com.livable.server.core.util.JwtTokenProvider;
+import com.livable.server.core.util.LoginActor;
+import com.livable.server.member.dto.MemberResponse;
+import com.livable.server.member.service.MemberService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RequiredArgsConstructor
+@RestController
+public class MemberController {
+
+ private final MemberService memberService;
+
+ @GetMapping("/api/members")
+ public ResponseEntity> myPage(@LoginActor Actor actor) {
+
+ JwtTokenProvider.checkMemberToken(actor);
+
+ Long memberId = actor.getId();
+ MemberResponse.MyPageDTO myPageDTO = memberService.getMyPageData(memberId);
+
+ return ApiResponse.success(myPageDTO, HttpStatus.OK);
+ }
+}
diff --git a/src/main/java/com/livable/server/member/domain/MemberErrorCode.java b/src/main/java/com/livable/server/member/domain/MemberErrorCode.java
new file mode 100644
index 00000000..27b7d380
--- /dev/null
+++ b/src/main/java/com/livable/server/member/domain/MemberErrorCode.java
@@ -0,0 +1,26 @@
+package com.livable.server.member.domain;
+
+import com.livable.server.core.exception.ErrorCode;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public enum MemberErrorCode implements ErrorCode {
+
+ MEMBER_NOT_EXIST(HttpStatus.BAD_REQUEST, "존재하지 않는 회원 정보입니다."),
+
+ BUILDING_INFO_NOT_EXIST(HttpStatus.BAD_REQUEST, "해당 회원의 빌딩 정보가 존재하지 않습니다."),
+
+ INVALID_TOKEN(HttpStatus.UNAUTHORIZED, "잘못된 토큰 정보입니다."),
+
+ INVALID_ACTOR_TYPE(HttpStatus.BAD_REQUEST, "존재하지 않는 유형의 사용자입니다."),
+
+ RETRIEVE_ACCESSCARD_FAILED(HttpStatus.BAD_REQUEST, "출입 카드 정보를 조회 할 수 없습니다.");
+
+ private final HttpStatus httpStatus;
+ private final String message;
+
+}
diff --git a/src/main/java/com/livable/server/member/dto/AccessCardProjection.java b/src/main/java/com/livable/server/member/dto/AccessCardProjection.java
new file mode 100644
index 00000000..fcd86984
--- /dev/null
+++ b/src/main/java/com/livable/server/member/dto/AccessCardProjection.java
@@ -0,0 +1,17 @@
+package com.livable.server.member.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class AccessCardProjection {
+
+ String buildingName;
+ String employeeNumber;
+ String companyName;
+ String floor;
+ String roomNumber;
+ String employeeName;
+
+}
diff --git a/src/main/java/com/livable/server/member/dto/BuildingInfoProjection.java b/src/main/java/com/livable/server/member/dto/BuildingInfoProjection.java
new file mode 100644
index 00000000..517abeb2
--- /dev/null
+++ b/src/main/java/com/livable/server/member/dto/BuildingInfoProjection.java
@@ -0,0 +1,14 @@
+package com.livable.server.member.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class BuildingInfoProjection {
+
+ Long buildingId;
+ String buildingName;
+ Boolean hasCafeteria;
+
+}
diff --git a/src/main/java/com/livable/server/member/dto/MemberResponse.java b/src/main/java/com/livable/server/member/dto/MemberResponse.java
new file mode 100644
index 00000000..ff1e757d
--- /dev/null
+++ b/src/main/java/com/livable/server/member/dto/MemberResponse.java
@@ -0,0 +1,24 @@
+package com.livable.server.member.dto;
+
+import lombok.*;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MemberResponse {
+
+ @Getter
+ @Builder
+ public static class MyPageDTO {
+
+ String memberName;
+ String companyName;
+ Integer pointValance;
+
+ public static MyPageDTO from(MyPageProjection myPageProjection) {
+ return MyPageDTO.builder()
+ .memberName(myPageProjection.getMemberName())
+ .companyName(myPageProjection.getCompanyName())
+ .pointValance(myPageProjection.getPointValance())
+ .build();
+ }
+ }
+}
diff --git a/src/main/java/com/livable/server/member/dto/MyPageProjection.java b/src/main/java/com/livable/server/member/dto/MyPageProjection.java
new file mode 100644
index 00000000..a76af197
--- /dev/null
+++ b/src/main/java/com/livable/server/member/dto/MyPageProjection.java
@@ -0,0 +1,14 @@
+package com.livable.server.member.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.ToString;
+
+@Getter
+@AllArgsConstructor
+public class MyPageProjection {
+
+ String memberName;
+ String companyName;
+ Integer pointValance;
+}
diff --git a/src/main/java/com/livable/server/member/repository/MemberRepository.java b/src/main/java/com/livable/server/member/repository/MemberRepository.java
new file mode 100644
index 00000000..1a2bedf0
--- /dev/null
+++ b/src/main/java/com/livable/server/member/repository/MemberRepository.java
@@ -0,0 +1,39 @@
+package com.livable.server.member.repository;
+
+import com.livable.server.entity.Member;
+import com.livable.server.member.dto.AccessCardProjection;
+import com.livable.server.member.dto.BuildingInfoProjection;
+import com.livable.server.member.dto.MyPageProjection;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+public interface MemberRepository extends JpaRepository {
+
+ @Query("SELECT new com.livable.server.member.dto.MyPageProjection(m.name, c.name, p.balance) " +
+ "FROM Member m " +
+ "INNER JOIN Company c ON c.id = m.company.id " +
+ "INNER JOIN Point p ON p.member.id = m.id " +
+ "WHERE m.id = :memberId")
+ Optional findMemberCompanyPointData(@Param("memberId") Long memberId);
+
+ @Query("SELECT distinct new com.livable.server.member.dto.AccessCardProjection(b.name, m.employeeNumber, c.name, o.floor, o.roomNumber, m.name) " +
+ "FROM Member m " +
+ "INNER JOIN Company c ON c.id = m.company.id " +
+ "INNER JOIN Office o ON c.id = o.company.id " +
+ "INNER JOIN Building b ON b.id = c.building.id " +
+ "WHERE m.id = :memberId")
+ List findAccessCardData(@Param("memberId") Long memberId);
+
+ @Query("SELECT new com.livable.server.member.dto.BuildingInfoProjection (b.id, b.name, b.hasCafeteria) " +
+ "FROM Member m " +
+ "JOIN Company c " +
+ "ON m.company = c " +
+ "JOIN Building b " +
+ "ON c.building = b " +
+ "WHERE m.id = :memberId")
+ Optional findBuildingInfoByMemberId(@Param("memberId") Long memberId);
+
+}
diff --git a/src/main/java/com/livable/server/member/service/MemberService.java b/src/main/java/com/livable/server/member/service/MemberService.java
new file mode 100644
index 00000000..28f97b5f
--- /dev/null
+++ b/src/main/java/com/livable/server/member/service/MemberService.java
@@ -0,0 +1,51 @@
+package com.livable.server.member.service;
+
+import com.livable.server.core.exception.GlobalRuntimeException;
+import com.livable.server.home.dto.HomeResponse;
+import com.livable.server.home.dto.HomeResponse.AccessCardDto;
+import com.livable.server.member.domain.MemberErrorCode;
+import com.livable.server.member.dto.AccessCardProjection;
+import com.livable.server.member.dto.BuildingInfoProjection;
+import com.livable.server.member.dto.MemberResponse;
+import com.livable.server.member.dto.MyPageProjection;
+import com.livable.server.member.repository.MemberRepository;
+import java.util.List;
+import java.util.Optional;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@RequiredArgsConstructor
+@Service
+public class MemberService {
+
+ private final MemberRepository memberRepository;
+
+ public MemberResponse.MyPageDTO getMyPageData(Long memberId) {
+
+ Optional myPageProjectionOption = memberRepository.findMemberCompanyPointData(memberId);
+ MyPageProjection myPageProjection = myPageProjectionOption.orElseThrow(()
+ -> new GlobalRuntimeException(MemberErrorCode.MEMBER_NOT_EXIST));
+
+ return MemberResponse.MyPageDTO.from(myPageProjection);
+ }
+
+ public HomeResponse.AccessCardDto getAccessCardData(Long memberId) {
+
+ List accessCardProjectionOptional = memberRepository.findAccessCardData(memberId);
+
+ if (accessCardProjectionOptional.isEmpty()) {
+ throw new GlobalRuntimeException(MemberErrorCode.RETRIEVE_ACCESSCARD_FAILED);
+ }
+
+ return AccessCardDto.from(accessCardProjectionOptional.get(0));
+ }
+
+ public HomeResponse.BuildingInfoDto getBuildingInfo(Long memberId) {
+ Optional buildingInfoProjectionOptional = memberRepository.findBuildingInfoByMemberId(memberId);
+ BuildingInfoProjection buildingInfoProjection = buildingInfoProjectionOptional.orElseThrow(()
+ -> new GlobalRuntimeException(MemberErrorCode.BUILDING_INFO_NOT_EXIST));
+
+ return HomeResponse.BuildingInfoDto.from(buildingInfoProjection);
+ }
+
+}
diff --git a/src/main/java/com/livable/server/menu/controller/MenuController.java b/src/main/java/com/livable/server/menu/controller/MenuController.java
new file mode 100644
index 00000000..1a38f5dc
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/controller/MenuController.java
@@ -0,0 +1,69 @@
+package com.livable.server.menu.controller;
+
+import static com.livable.server.menu.domain.MenuPaging.MOST_SELECTED_MENU;
+
+import com.livable.server.core.response.ApiResponse;
+import com.livable.server.core.response.ApiResponse.Success;
+import com.livable.server.core.util.Actor;
+import com.livable.server.core.util.JwtTokenProvider;
+import com.livable.server.core.util.LoginActor;
+import com.livable.server.menu.dto.MenuRequest.MenuChoiceLogDTO;
+import com.livable.server.menu.dto.MenuResponse.MostSelectedMenuDTO;
+import com.livable.server.menu.dto.MenuResponse.RouletteMenuDTO;
+import com.livable.server.menu.service.MenuService;
+import java.util.List;
+import javax.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RequiredArgsConstructor
+@RestController
+public class MenuController {
+
+ private final MenuService menuService;
+
+ @GetMapping("/api/menus")
+ public ResponseEntity>> getRouletteMenus(@LoginActor Actor actor) {
+
+ JwtTokenProvider.checkMemberToken(actor);
+
+ List rouletteMenuDTOs = menuService.getRouletteMenus();
+
+ return ApiResponse.success(rouletteMenuDTOs, HttpStatus.OK);
+ }
+
+ @GetMapping("/api/menus/choices")
+ public ResponseEntity>> getMostSelectedMenu(@RequestParam("buildingId") Long buildingId, @LoginActor Actor actor) {
+
+ JwtTokenProvider.checkMemberToken(actor);
+
+ //추후 가져 오는 메뉴 숫자 변경시 변경
+ Pageable pageable = PageRequest.of(0, MOST_SELECTED_MENU.getLimit());
+
+ List mostSelectedMenu = menuService.getMostSelectedMenu(buildingId, pageable);
+
+ return ApiResponse.success(mostSelectedMenu, HttpStatus.OK);
+ }
+
+ @PostMapping("/api/menus/choices")
+ public ResponseEntity> createMenuChoiceLog(@Valid @RequestBody MenuChoiceLogDTO menuChoiceLogDTO, @LoginActor Actor actor) {
+
+ JwtTokenProvider.checkMemberToken(actor);
+
+ Long memberId = actor.getId();
+
+ //오늘 이미 룰렛을 돌렸다면 결과를 반영하지 않고 201 return
+ menuService.createMenuChoiceLog(memberId, menuChoiceLogDTO);
+
+ return ApiResponse.success(HttpStatus.CREATED);
+ }
+
+}
diff --git a/src/main/java/com/livable/server/menu/domain/MenuChoiceResultDateRange.java b/src/main/java/com/livable/server/menu/domain/MenuChoiceResultDateRange.java
new file mode 100644
index 00000000..380fc387
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/domain/MenuChoiceResultDateRange.java
@@ -0,0 +1,32 @@
+package com.livable.server.menu.domain;
+
+import java.time.DayOfWeek;
+import java.time.LocalDate;
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class MenuChoiceResultDateRange {
+
+ private LocalDate startDate;
+ private LocalDate endDate;
+
+ public static MenuChoiceResultDateRange getDateRange(LocalDate referenceDate) {
+
+ LocalDate startDate;
+ LocalDate endDate;
+
+ startDate = referenceDate.minusDays(
+ referenceDate.getDayOfWeek().getValue() - 1L)
+ .minusWeeks(1);
+
+ endDate = startDate.plusDays(6);
+
+ return MenuChoiceResultDateRange.builder()
+ .startDate(startDate)
+ .endDate(endDate)
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/livable/server/menu/domain/MenuErrorCode.java b/src/main/java/com/livable/server/menu/domain/MenuErrorCode.java
new file mode 100644
index 00000000..dda1a7bf
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/domain/MenuErrorCode.java
@@ -0,0 +1,22 @@
+package com.livable.server.menu.domain;
+
+import com.livable.server.core.exception.ErrorCode;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.http.HttpStatus;
+
+@Getter
+@AllArgsConstructor(access = AccessLevel.PRIVATE)
+public enum MenuErrorCode implements ErrorCode {
+
+ RETRIEVE_ROULETTE_MENU_FAILED(HttpStatus.BAD_REQUEST, "룰렛 메뉴 정보를 불러 올 수 없습니다."),
+
+ MENU_NOT_EXIST(HttpStatus.BAD_REQUEST, "메뉴를 찾을 수 없습니다."),
+
+ BUILDING_NOT_VALID(HttpStatus.BAD_REQUEST, "빌딩 정보가 일치 하지 않습니다.");
+
+ private final HttpStatus httpStatus;
+ private final String message;
+
+}
diff --git a/src/main/java/com/livable/server/menu/domain/MenuPaging.java b/src/main/java/com/livable/server/menu/domain/MenuPaging.java
new file mode 100644
index 00000000..356bd2f4
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/domain/MenuPaging.java
@@ -0,0 +1,14 @@
+package com.livable.server.menu.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum MenuPaging {
+
+ MOST_SELECTED_MENU(10);
+
+ private final Integer limit;
+
+}
diff --git a/src/main/java/com/livable/server/menu/domain/MenuValidationMessage.java b/src/main/java/com/livable/server/menu/domain/MenuValidationMessage.java
new file mode 100644
index 00000000..254610eb
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/domain/MenuValidationMessage.java
@@ -0,0 +1,6 @@
+package com.livable.server.menu.domain;
+
+public interface MenuValidationMessage {
+ String NOT_NULL = "값이 Null 일수 없습니다.";
+
+}
diff --git a/src/main/java/com/livable/server/menu/dto/MenuChoiceProjection.java b/src/main/java/com/livable/server/menu/dto/MenuChoiceProjection.java
new file mode 100644
index 00000000..9e6f88d4
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/dto/MenuChoiceProjection.java
@@ -0,0 +1,18 @@
+package com.livable.server.menu.dto;
+
+import com.livable.server.entity.Building;
+import com.livable.server.entity.Menu;
+import java.time.LocalDate;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class MenuChoiceProjection {
+
+ Building building;
+ Menu menu;
+ LocalDate date;
+ Long count;
+
+}
diff --git a/src/main/java/com/livable/server/menu/dto/MenuRequest.java b/src/main/java/com/livable/server/menu/dto/MenuRequest.java
new file mode 100644
index 00000000..401e3486
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/dto/MenuRequest.java
@@ -0,0 +1,34 @@
+package com.livable.server.menu.dto;
+
+
+import static com.livable.server.menu.domain.MenuValidationMessage.NOT_NULL;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.livable.server.core.util.PresentDate;
+import java.time.LocalDate;
+import javax.validation.constraints.NotNull;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MenuRequest {
+
+ @Getter
+ @Builder
+ @NoArgsConstructor
+ @AllArgsConstructor
+ @JsonIgnoreProperties(ignoreUnknown = true)
+ public static class MenuChoiceLogDTO {
+
+ @NotNull(message = NOT_NULL)
+ private Long menuId;
+
+ @PresentDate
+ private LocalDate date;
+
+ }
+
+}
diff --git a/src/main/java/com/livable/server/menu/dto/MenuResponse.java b/src/main/java/com/livable/server/menu/dto/MenuResponse.java
new file mode 100644
index 00000000..0d649011
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/dto/MenuResponse.java
@@ -0,0 +1,48 @@
+package com.livable.server.menu.dto;
+
+import java.time.LocalDate;
+import java.util.List;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor(access = AccessLevel.PRIVATE)
+public class MenuResponse {
+
+ @Getter
+ @AllArgsConstructor
+ public static class RouletteMenuDTO {
+
+ private String categoryName;
+ private List menus;
+
+ }
+
+ @Getter
+ @Builder
+ @AllArgsConstructor
+ public static class MostSelectedMenuDTO {
+
+ private LocalDate date;
+ private Integer count;
+ private Integer rank;
+ private Long menuId;
+ private String menuName;
+ private String menuImage;
+
+ public static MostSelectedMenuDTO from(MostSelectedMenuProjection mostSelectedMenuProjection, Integer rank) {
+ return MostSelectedMenuDTO.builder()
+ .date(mostSelectedMenuProjection.getDate())
+ .count(mostSelectedMenuProjection.getCount())
+ .rank(rank)
+ .menuId(mostSelectedMenuProjection.getMenuId())
+ .menuName(mostSelectedMenuProjection.menuName)
+ .menuImage(mostSelectedMenuProjection.getMenuImage())
+ .build();
+ }
+
+ }
+
+}
diff --git a/src/main/java/com/livable/server/menu/dto/MostSelectedMenuProjection.java b/src/main/java/com/livable/server/menu/dto/MostSelectedMenuProjection.java
new file mode 100644
index 00000000..910935ab
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/dto/MostSelectedMenuProjection.java
@@ -0,0 +1,17 @@
+package com.livable.server.menu.dto;
+
+import java.time.LocalDate;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class MostSelectedMenuProjection {
+
+ Integer count;
+ LocalDate date;
+ Long menuId;
+ String menuName;
+ String menuImage;
+
+}
diff --git a/src/main/java/com/livable/server/menu/dto/RouletteMenu.java b/src/main/java/com/livable/server/menu/dto/RouletteMenu.java
new file mode 100644
index 00000000..ec8ab912
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/dto/RouletteMenu.java
@@ -0,0 +1,20 @@
+package com.livable.server.menu.dto;
+
+import lombok.Builder;
+import lombok.Getter;
+
+@Getter
+@Builder
+public class RouletteMenu {
+
+ private Long menuId;
+ private String name;
+
+ public static RouletteMenu from(RouletteMenuProjection rouletteMenuProjection) {
+ return RouletteMenu.builder()
+ .menuId(rouletteMenuProjection.getMenuId())
+ .name(rouletteMenuProjection.getName())
+ .build();
+ }
+
+}
diff --git a/src/main/java/com/livable/server/menu/dto/RouletteMenuProjection.java b/src/main/java/com/livable/server/menu/dto/RouletteMenuProjection.java
new file mode 100644
index 00000000..e1fa262c
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/dto/RouletteMenuProjection.java
@@ -0,0 +1,14 @@
+package com.livable.server.menu.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public class RouletteMenuProjection {
+
+ Long menuId;
+ String name;
+ String menuCategoryName;
+
+}
diff --git a/src/main/java/com/livable/server/menu/exception/.gitkeep b/src/main/java/com/livable/server/menu/exception/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/src/main/java/com/livable/server/menu/repository/MenuChoiceLogRepository.java b/src/main/java/com/livable/server/menu/repository/MenuChoiceLogRepository.java
new file mode 100644
index 00000000..74f18d41
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/repository/MenuChoiceLogRepository.java
@@ -0,0 +1,25 @@
+package com.livable.server.menu.repository;
+
+import com.livable.server.entity.MenuChoiceLog;
+import com.livable.server.menu.dto.MenuChoiceProjection;
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface MenuChoiceLogRepository extends JpaRepository {
+
+ Optional findByMemberIdAndDate(Long memberId, LocalDate date);
+
+ @Query("SELECT new com.livable.server.menu.dto.MenuChoiceProjection(mcl.building, mcl.menu, mcl.date, COUNT(mcl.menu.id)) " +
+ "FROM MenuChoiceLog mcl " +
+ "WHERE mcl.date = :referenceDate " +
+ "GROUP BY mcl.building, mcl.menu, mcl.date " +
+ "ORDER BY count(mcl.menu.id) DESC")
+ List findMenuChoiceLog(@Param("referenceDate") LocalDate referenceDate);
+
+}
diff --git a/src/main/java/com/livable/server/menu/repository/MenuChoiceResultRepository.java b/src/main/java/com/livable/server/menu/repository/MenuChoiceResultRepository.java
new file mode 100644
index 00000000..0ace18ce
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/repository/MenuChoiceResultRepository.java
@@ -0,0 +1,18 @@
+package com.livable.server.menu.repository;
+
+import com.livable.server.entity.MenuChoiceResult;
+import com.livable.server.menu.dto.MenuChoiceProjection;
+import java.time.LocalDate;
+import java.util.List;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+
+public interface MenuChoiceResultRepository extends JpaRepository {
+ @Query("SELECT new com.livable.server.menu.dto.MenuChoiceProjection(mcr.building, mcr.menu, mcr.date, sum(mcr.count)) " +
+ "FROM MenuChoiceResult mcr " +
+ "WHERE mcr.date between :startDate AND :endDate " +
+ "GROUP BY mcr.building, mcr.menu, mcr.date " +
+ "ORDER BY sum(mcr.count) DESC")
+ List findMenuChoiceResult(@Param("startDate") LocalDate startDate, @Param("endDate") LocalDate endDate);
+}
diff --git a/src/main/java/com/livable/server/menu/repository/MenuChoiceWeeklyResultRepository.java b/src/main/java/com/livable/server/menu/repository/MenuChoiceWeeklyResultRepository.java
new file mode 100644
index 00000000..74074286
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/repository/MenuChoiceWeeklyResultRepository.java
@@ -0,0 +1,8 @@
+package com.livable.server.menu.repository;
+
+import com.livable.server.entity.MenuChoiceWeeklyResult;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+public interface MenuChoiceWeeklyResultRepository extends JpaRepository {
+
+}
diff --git a/src/main/java/com/livable/server/menu/repository/MenuRepository.java b/src/main/java/com/livable/server/menu/repository/MenuRepository.java
new file mode 100644
index 00000000..9c813d52
--- /dev/null
+++ b/src/main/java/com/livable/server/menu/repository/MenuRepository.java
@@ -0,0 +1,41 @@
+package com.livable.server.menu.repository;
+
+import com.livable.server.entity.Menu;
+import com.livable.server.menu.dto.MostSelectedMenuProjection;
+import com.livable.server.menu.dto.RouletteMenuProjection;
+import java.time.LocalDate;
+import java.util.List;
+import org.springframework.data.domain.Pageable;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
+import org.springframework.stereotype.Repository;
+
+@Repository
+public interface MenuRepository extends JpaRepository