diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..f91f64602 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +# Binary files should be left untouched +*.jar binary + diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..9d77ee124 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### 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/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +docker/db/ \ No newline at end of file diff --git a/README.md b/README.md index 5fcc66b4d..9c140d2e5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,47 @@ -## [본 과정] 이커머스 핵심 프로세스 구현 -[단기 스킬업 Redis 교육 과정](https://hh-skillup.oopy.io/) 을 통해 상품 조회 및 주문 과정을 구현하며 현업에서 발생하는 문제를 Redis의 핵심 기술을 통해 해결합니다. -> Indexing, Caching을 통한 성능 개선 / 단계별 락 구현을 통한 동시성 이슈 해결 (낙관적/비관적 락, 분산락 등) +## Multi Module Design + +#### **의존관계** +- `presentation` → `application` → `infrastructure` +- `application` → `domain` +- `infrastructure` → `domain` + +#### **모듈별 역할** +- 각 모듈의 상세 설명은 해당 모듈 디렉토리 내 README.md 파일을 참고하십시오. + +--- + +## Table Design +- 테이블 구조는 아래의 다이어그램으로 대체됩니다. +- **![img.png](img.png)** + +--- + +## Architecture + +- **Port-Adapter 패턴 적용** + - 각 모듈 간 결합을 제거하여 독립성을 유지합니다. + - `Application` 계층은 비즈니스 로직만 담당하며, 외부 기술에 의존하지 않습니다. + - `Infrastructure` 계층은 외부 기술(JPA, DB, API 등)을 담당하며, Port를 통해 `Application`과 통신합니다. + +--- + +## API Design + +#### **`GET /api/v1/screenings`** +- 최신 영화별로 그룹핑하여 빠른 시간순으로 정렬된 상영 영화 목록을 반환합니다. +- 기본적으로 오늘부터 2일 이내 상영 영화 목록을 반환하며, 클라이언트 요청에 따라 기간을 조정할 수 있습니다. +- **HTTP 요청 예시**: + - IntelliJ Http Client `http/getScreenings.http` 참고. + +--- + +## 프로젝트 주요 특징 +- **모듈화된 설계**: 명확한 책임 분리를 통해 유지보수와 확장성을 높임. +- **API 유연성**: 다양한 클라이언트 요청 시나리오를 지원할 수 있는 유연한 파라미터 설계. +- **테이블 설계와 아키텍처**: 프로젝트 구조와 데이터베이스 설계를 통해 높은 일관성과 성능을 유지. + +--- + +## 데이터 관리 +- **flyway**로 형상 관리. Movie, Genre, Seat, Cinema 엔티티 생성 및 기본 데이터 생성 +- Screening(상영 영화 시간표)는 ``CommandLineRunner``를 구현하여 프로잭트 구동 시 생성 diff --git a/build.gradle b/build.gradle new file mode 100644 index 000000000..29ac2bbdf --- /dev/null +++ b/build.gradle @@ -0,0 +1,65 @@ +plugins { + id 'java' + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'project.redis' +version = '0.0.1-SNAPSHOT' + + +repositories { + mavenCentral() +} + +subprojects { + apply plugin: 'java' + apply plugin: 'java-library' + apply plugin: 'io.spring.dependency-management' + + java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + dependencyManagement { + imports { + mavenBom 'org.springframework.boot:spring-boot-dependencies:3.4.1' + } + } + + configurations { + compileOnly { + extendsFrom annotationProcessor + } + } + + repositories { + mavenCentral() + } + + + dependencies { + compileOnly 'org.projectlombok:lombok:1.18.36' + annotationProcessor 'org.projectlombok:lombok:1.18.36' + + + testCompileOnly 'org.projectlombok:lombok:1.18.36' + testAnnotationProcessor 'org.projectlombok:lombok:1.18.36' + } + + tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + } + + tasks { + test { + useJUnitPlatform() + } + } +} + + + +jar { + enabled = false +} \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 000000000..6bc0c09ce --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,21 @@ +version: '3' +services: + redis-movie: + image: mysql:9.1.0 + container_name: cinema-mysql + restart: always + ports: + - "3309:3306" + volumes: + - ./db/conf.d:/etc/mysql/conf.d + - ./db/data:/var/lib/mysql + - ./db/initdb.d:/docker-entrypoint-initdb.d + environment: + - TZ=Asia/Seoul + - MYSQL_ROOT_PASSWORD=1234 + - MYSQL_DATABASE=redis-movie + - MYSQL_USER=hongs + - MYSQL_PASSWORD=local1234 + command: > + --character-set-server=utf8mb4 + --collation-server=utf8mb4_unicode_ci \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 000000000..1fab15cd4 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,3 @@ +#org.gradle.configuration-cache=true +#org.gradle.parallel=true +#org.gradle.caching=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..4ac3234a6 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,2 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 000000000..a4b76b953 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 000000000..e2847c820 --- /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.11.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 000000000..f5feea6d6 --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/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. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# 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/platforms/jvm/plugins-application/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##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || 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=SC2039,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=SC2039,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, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +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 000000000..9d21a2183 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@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 +@rem SPDX-License-Identifier: Apache-2.0 +@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. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +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/http/getScreenings.http b/http/getScreenings.http new file mode 100644 index 000000000..15eca66c5 --- /dev/null +++ b/http/getScreenings.http @@ -0,0 +1,12 @@ +### GET request to example server +GET https://examples.http-client.intellij.net/get + ?generated-in=IntelliJ IDEA + +### GET screenings (기본 2일) +GET http://localhost:8080/api/v1/screenings +Content-Type: application/json + + +### GET screenings (3일 이내 상영 영화 목록 가능) +GET http://localhost:8080/api/v1/screenings?maxScreeningDay=3 +Content-Type: application/json diff --git a/img.png b/img.png new file mode 100644 index 000000000..6fec5a76b Binary files /dev/null and b/img.png differ diff --git a/module-application/README.md b/module-application/README.md new file mode 100644 index 000000000..9dd3af66e --- /dev/null +++ b/module-application/README.md @@ -0,0 +1,105 @@ +## [Application Module] + +### 책임 + +1. **도메인 객체를 통해 Application 흐름 제어** + - 도메인 간 상호작용을 조율하며 비즈니스 로직의 흐름을 제어. + - 각 도메인 서비스와 협력하여 비즈니스 요구사항을 처리. + +2. **Infrastructure 모듈과의 분리** + - 데이터베이스와의 직접적인 통신은 Infrastructure 모듈에 위임. + - `ScreeningQueryPort`와 같은 인터페이스를 통해 Application 계층은 외부 기술에 독립적. + +3. **외부 기술과 무관한 설계** + - Infrastructure에 종속되지 않으며, 비즈니스 로직의 흐름에만 집중. + - 외부 기술 변경(JPA -> NoSQL 등)이 Application 계층에 영향을 미치지 않도록 설계. + +--- + +### 구조 + +- **Port-Adapter 패턴 적용**: + - Application 계층은 Port(인터페이스)를 정의하며, 실제 구현은 Infrastructure 모듈에서 Adapter로 처리. + - 예: `ScreeningQueryPort`를 통해 데이터 소스를 추상화. + +- **도메인 간 상호작용**: + - 비즈니스 로직은 도메인 객체와 도메인 서비스를 활용하여 처리. + - 데이터 변환 및 조율을 담당. + +--- + +### 주요 클래스 + +#### **`ScreeningQueryService`** +- 비즈니스 로직의 흐름을 제어하는 서비스 클래스. +- Port 인터페이스(`ScreeningQueryPort`)를 통해 Infrastructure 모듈과 통신. + +```java +@Service +@RequiredArgsConstructor +public class ScreeningQueryService implements ScreeningQueryUseCase { + + private final ScreeningQueryPort screeningQueryPort; + + @Override + public List getScreenings(ScreeningsQueryParam param) { + return screeningQueryPort.getScreenings(param.getMaxScreeningDay()); + } +} +``` + +--- + +#### **`ScreeningQueryUseCase`** +- Application 계층의 핵심 인터페이스. +- Presentation 계층이 이를 통해 비즈니스 로직에 접근. + +```java +public interface ScreeningQueryUseCase { + + List getScreenings(ScreeningsQueryParam param); + +} +``` + +--- + +#### **`ScreeningsQueryParam`** +- 비즈니스 로직 수행에 필요한 파라미터를 캡슐화한 클래스. + +```java +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ScreeningsQueryParam { + private int maxScreeningDay; +} +``` + +--- + +### 설계 원칙 + +1. **Port-Adapter 패턴 준수**: + - Port(인터페이스): `ScreeningQueryPort`. + - Adapter(구현체): Infrastructure 모듈에서 구현. + +2. **도메인 중심 설계**: + - 비즈니스 로직은 도메인 객체 및 도메인 서비스와 협력하여 처리. + - Application 계층은 비즈니스 흐름에 집중. + +3. **기술 독립성**: + - Application 계층은 외부 기술(JPA, DB 등)에 종속되지 않으며, 순수 Java 로직으로 구성. + +4. **확장성**: + - 새로운 데이터 소스나 비즈니스 요구사항이 추가되더라도 Port와 Adapter를 통해 확장 가능. + +--- + +### 장점 +- **유지보수성**: 외부 기술 변경 시 Application 계층에는 영향을 미치지 않음. +- **테스트 용이성**: Port를 Mocking하여 테스트 가능. +- **독립성**: 기술과 무관한 순수 비즈니스 로직 유지. + +--- diff --git a/module-application/build.gradle b/module-application/build.gradle new file mode 100644 index 000000000..4fcf7c336 --- /dev/null +++ b/module-application/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'org.springframework.boot' version '3.4.1' +} + +jar { + enabled = true +} + +bootJar { + enabled = false +} + +bootRun { + enabled = false +} + + +group = 'project.redis.application' +version = '0.0.1-SNAPSHOT' + +dependencies { + implementation project(':module-domain') + implementation project(':module-infrastructure') + implementation 'org.springframework.boot:spring-boot-starter' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} \ No newline at end of file diff --git a/module-application/src/main/java/project/redis/application/cinema/port/inbound/CinemaCommandUseCase.java b/module-application/src/main/java/project/redis/application/cinema/port/inbound/CinemaCommandUseCase.java new file mode 100644 index 000000000..190c682ca --- /dev/null +++ b/module-application/src/main/java/project/redis/application/cinema/port/inbound/CinemaCommandUseCase.java @@ -0,0 +1,6 @@ +package project.redis.application.cinema.port.inbound; + +public interface CinemaCommandUseCase { + + void createCinema(CinemaCreateCommandParam param); +} diff --git a/module-application/src/main/java/project/redis/application/cinema/port/inbound/CinemaCreateCommandParam.java b/module-application/src/main/java/project/redis/application/cinema/port/inbound/CinemaCreateCommandParam.java new file mode 100644 index 000000000..7bc856aa0 --- /dev/null +++ b/module-application/src/main/java/project/redis/application/cinema/port/inbound/CinemaCreateCommandParam.java @@ -0,0 +1,14 @@ +package project.redis.application.cinema.port.inbound; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CinemaCreateCommandParam { + private String CinemaName; +} diff --git a/module-application/src/main/java/project/redis/application/cinema/port/inbound/CinemaQueryUseCase.java b/module-application/src/main/java/project/redis/application/cinema/port/inbound/CinemaQueryUseCase.java new file mode 100644 index 000000000..e0b1a7f5b --- /dev/null +++ b/module-application/src/main/java/project/redis/application/cinema/port/inbound/CinemaQueryUseCase.java @@ -0,0 +1,9 @@ +package project.redis.application.cinema.port.inbound; + +import java.util.List; +import project.redis.domain.cinema.Cinema; + +public interface CinemaQueryUseCase { + + List getCinemas(); +} diff --git a/module-application/src/main/java/project/redis/application/cinema/service/CinemaCommandService.java b/module-application/src/main/java/project/redis/application/cinema/service/CinemaCommandService.java new file mode 100644 index 000000000..51d4d10d0 --- /dev/null +++ b/module-application/src/main/java/project/redis/application/cinema/service/CinemaCommandService.java @@ -0,0 +1,21 @@ +package project.redis.application.cinema.service; + + +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import project.redis.application.cinema.port.inbound.CinemaCommandUseCase; +import project.redis.application.cinema.port.inbound.CinemaCreateCommandParam; +import project.redis.infrastructure.cinema.inbound.port.CinemaCommandPort; + + +@Service +@RequiredArgsConstructor +public class CinemaCommandService implements CinemaCommandUseCase { + + private final CinemaCommandPort cinemaCommandPort; + + @Override + public void createCinema(CinemaCreateCommandParam param) { + cinemaCommandPort.createCinema(param.getCinemaName()); + } +} diff --git a/module-application/src/main/java/project/redis/application/cinema/service/CinemaQueryService.java b/module-application/src/main/java/project/redis/application/cinema/service/CinemaQueryService.java new file mode 100644 index 000000000..a11533730 --- /dev/null +++ b/module-application/src/main/java/project/redis/application/cinema/service/CinemaQueryService.java @@ -0,0 +1,21 @@ +package project.redis.application.cinema.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import project.redis.application.cinema.port.inbound.CinemaQueryUseCase; +import project.redis.domain.cinema.Cinema; +import project.redis.infrastructure.cinema.inbound.port.CinemaQueryPort; + + +@Service +@RequiredArgsConstructor +public class CinemaQueryService implements CinemaQueryUseCase { + + private final CinemaQueryPort cinemaQueryPort; + + @Override + public List getCinemas() { + return cinemaQueryPort.getCinemas(); + } +} diff --git a/module-application/src/main/java/project/redis/application/movie/port/inbound/MovieQueryUseCase.java b/module-application/src/main/java/project/redis/application/movie/port/inbound/MovieQueryUseCase.java new file mode 100644 index 000000000..e7dcc419b --- /dev/null +++ b/module-application/src/main/java/project/redis/application/movie/port/inbound/MovieQueryUseCase.java @@ -0,0 +1,9 @@ +package project.redis.application.movie.port.inbound; + +import java.util.List; +import project.redis.domain.movie.Movie; + +public interface MovieQueryUseCase { + + List getMovies(); +} diff --git a/module-application/src/main/java/project/redis/application/movie/service/MovieQueryService.java b/module-application/src/main/java/project/redis/application/movie/service/MovieQueryService.java new file mode 100644 index 000000000..e860d2591 --- /dev/null +++ b/module-application/src/main/java/project/redis/application/movie/service/MovieQueryService.java @@ -0,0 +1,21 @@ +package project.redis.application.movie.service; + + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import project.redis.application.movie.port.inbound.MovieQueryUseCase; +import project.redis.domain.movie.Movie; +import project.redis.infrastructure.movie.inbound.port.MovieQueryPort; + +@Service +@RequiredArgsConstructor +public class MovieQueryService implements MovieQueryUseCase { + + private final MovieQueryPort movieQueryPort; + + @Override + public List getMovies() { + return movieQueryPort.getMovies(); + } +} diff --git a/module-application/src/main/java/project/redis/application/screening/port/inbound/ScreeningQueryUseCase.java b/module-application/src/main/java/project/redis/application/screening/port/inbound/ScreeningQueryUseCase.java new file mode 100644 index 000000000..e68a896ca --- /dev/null +++ b/module-application/src/main/java/project/redis/application/screening/port/inbound/ScreeningQueryUseCase.java @@ -0,0 +1,9 @@ +package project.redis.application.screening.port.inbound; + +import java.util.List; +import project.redis.domain.screening.Screening; + +public interface ScreeningQueryUseCase { + + List getScreenings(ScreeningsQueryParam param); +} diff --git a/module-application/src/main/java/project/redis/application/screening/port/inbound/ScreeningsQueryParam.java b/module-application/src/main/java/project/redis/application/screening/port/inbound/ScreeningsQueryParam.java new file mode 100644 index 000000000..47744e494 --- /dev/null +++ b/module-application/src/main/java/project/redis/application/screening/port/inbound/ScreeningsQueryParam.java @@ -0,0 +1,14 @@ +package project.redis.application.screening.port.inbound; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ScreeningsQueryParam { + private int maxScreeningDay; +} diff --git a/module-application/src/main/java/project/redis/application/screening/service/ScreeningQueryService.java b/module-application/src/main/java/project/redis/application/screening/service/ScreeningQueryService.java new file mode 100644 index 000000000..43f29ae12 --- /dev/null +++ b/module-application/src/main/java/project/redis/application/screening/service/ScreeningQueryService.java @@ -0,0 +1,23 @@ +package project.redis.application.screening.service; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import project.redis.application.screening.port.inbound.ScreeningQueryUseCase; +import project.redis.application.screening.port.inbound.ScreeningsQueryParam; +import project.redis.domain.screening.Screening; +import project.redis.infrastructure.screening.inbound.port.inbound.ScreeningQueryPort; + + + +@Service +@RequiredArgsConstructor +public class ScreeningQueryService implements ScreeningQueryUseCase { + + private final ScreeningQueryPort screeningQueryPort; + + @Override + public List getScreenings(ScreeningsQueryParam param) { + return screeningQueryPort.getScreenings(param.getMaxScreeningDay()); + } +} diff --git a/module-application/src/test/java/project/redis/application/cinema/service/CinemaCommandServiceTest.java b/module-application/src/test/java/project/redis/application/cinema/service/CinemaCommandServiceTest.java new file mode 100644 index 000000000..87785466b --- /dev/null +++ b/module-application/src/test/java/project/redis/application/cinema/service/CinemaCommandServiceTest.java @@ -0,0 +1,59 @@ +package project.redis.application.cinema.service; + + +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import project.redis.application.cinema.port.inbound.CinemaCreateCommandParam; +import project.redis.infrastructure.cinema.inbound.port.CinemaCommandPort; + +@ExtendWith(MockitoExtension.class) +class CinemaCommandServiceTest { + + @Mock + CinemaCommandPort cinemaCommandPort; + + @InjectMocks + CinemaCommandService cinemaCommandService; + + + @DisplayName("상영관 생성 - 성공") + @Test + void testCreateCinema() { + String cinemaName = "cinema"; + CinemaCreateCommandParam param = CinemaCreateCommandParam.builder() + .CinemaName(cinemaName) + .build(); + + doNothing().when(cinemaCommandPort).createCinema(param.getCinemaName()); + + cinemaCommandService.createCinema(param); + + verify(cinemaCommandPort).createCinema(param.getCinemaName()); + } + + @DisplayName("상영관 생성 - 실패 - 이미 존재하는 상영관 이름") + @Test + void testCreateCinemaWithInvalidCinemaName() { + String cinemaName = "cinema"; + CinemaCreateCommandParam param = CinemaCreateCommandParam.builder() + .CinemaName(cinemaName) + .build(); + + doThrow(IllegalArgumentException.class) + .when(cinemaCommandPort).createCinema(param.getCinemaName()); + + Assertions.assertThrows(IllegalArgumentException.class, () -> cinemaCommandService.createCinema(param)); + + verify(cinemaCommandPort).createCinema(param.getCinemaName()); + } + +} \ No newline at end of file diff --git a/module-domain/README.md b/module-domain/README.md new file mode 100644 index 000000000..b59dad747 --- /dev/null +++ b/module-domain/README.md @@ -0,0 +1,97 @@ +## [Domain Module] + +### 책임 + +1. **상호작용하는 객체 관리** + - 영화(Screening), 영화관(Cinema), 영화 장르(Genre) 등 도메인 간의 상호작용을 관리. + - 각 도메인은 데이터베이스 Entity와 분리된 **비즈니스 중심 객체**로 구성됩니다. + +2. **도메인 로직 담당** + - 각 도메인 클래스는 비즈니스 로직을 내포하며, 객체 간의 유효성 검사 및 데이터 조작을 처리합니다. + - 예: 상영 시간이 유효한지 확인하거나 영화와 상영관 간의 연관성을 검증. + +3. **도메인 간 로직 처리 (추후 도메인 서비스 도입 예정)** + - 특정 도메인에 국한되지 않는 로직은 별도의 Domain Service로 분리하여 관리할 예정. + - 예: 영화 상영 시간표 생성 로직. + +--- + +### 구조 + +- **변경 불가능한 객체**: + - 모든 도메인 객체는 불변성을 유지하여 안정성과 일관성을 확보합니다. + - 예: `@Value`, 생성자 기반 객체 생성. + +- **Entity와의 분리**: + - 도메인 객체는 데이터베이스와의 의존성을 가지지 않으며, 기술적인 구현 세부사항을 포함하지 않습니다. + - Entity 클래스는 Infrastructure 계층에 존재하며, 도메인 클래스와 별도로 관리됩니다. + +--- + +### 주요 클래스 + +#### **`Screening`** +- 영화 상영 정보를 나타내는 도메인 클래스. +- 영화(`Movie`), 상영관(`Cinema`)과 연관. + +```java +@Getter +@Value +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Screening { + UUID screeningId; + LocalDateTime screenStartTime; + LocalDateTime screenEndTime; + Movie movie; + Cinema cinema; + + public static Screening generateScreening( + UUID screeningId, + LocalDateTime screenStartTime, LocalDateTime screenEndTime, + Movie movie, Cinema cinema) { + return new Screening(screeningId, screenStartTime, screenEndTime, movie, cinema); + } +} +``` + +--- + +#### **`Movie`** +- 영화 정보를 담는 도메인 클래스. + +```java +@Getter +@Value +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Movie { + UUID movieId; + String title; + RatingClassification rating; + LocalDate releaseDate; + Genre genre; + + public static Movie generateMovie( + UUID movieId, String title, + RatingClassification rating, LocalDate releaseDate, + Genre genre) { + return new Movie(movieId, title, rating, releaseDate, genre); + } +} +``` + +--- + +#### **설계 원칙** +1. **불변 객체**: + - 모든 필드는 `final`로 선언하여 객체의 상태 변경을 방지. + - 객체 생성은 정적 팩토리 메서드(`generateScreening`, `generateMovie`)를 통해 관리. + +2. **Entity와 도메인의 분리**: + - 데이터베이스와 상호작용하는 JPA Entity는 Infrastructure 계층에 존재. + - 도메인은 순수 비즈니스 로직만 포함하여 데이터베이스 구현 변경 시 영향을 최소화. + +3. **확장성**: + - 도메인 클래스에 새로운 필드나 로직을 추가할 때, 기존 로직의 영향을 최소화. + - Domain Service를 통해 도메인 간 복잡한 로직 분리 가능. + +--- diff --git a/module-domain/build.gradle b/module-domain/build.gradle new file mode 100644 index 000000000..724e0a6cb --- /dev/null +++ b/module-domain/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'io.spring.dependency-management' version '1.1.7' +} + +group = 'project.redis.domain' +version = '0.0.1-SNAPSHOT' + +dependencies { + + testImplementation("org.junit.jupiter:junit-jupiter-api") + testImplementation("org.junit.jupiter:junit-jupiter-params") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") + + testImplementation("org.assertj:assertj-core") + testImplementation("org.mockito:mockito-core") + testImplementation("org.mockito:mockito-junit-jupiter") +} diff --git a/module-domain/src/main/java/project/redis/domain/cinema/Cinema.java b/module-domain/src/main/java/project/redis/domain/cinema/Cinema.java new file mode 100644 index 000000000..3282e6c6e --- /dev/null +++ b/module-domain/src/main/java/project/redis/domain/cinema/Cinema.java @@ -0,0 +1,19 @@ +package project.redis.domain.cinema; + +import java.util.UUID; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Value; + +@Getter +@Value +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Cinema { + UUID cinemaId; + String cinemaName; + + public static Cinema generateCinema(UUID cinemaId, String cinemaName) { + return new Cinema(cinemaId, cinemaName); + } +} \ No newline at end of file diff --git a/module-domain/src/main/java/project/redis/domain/genre/Genre.java b/module-domain/src/main/java/project/redis/domain/genre/Genre.java new file mode 100644 index 000000000..14fa4c517 --- /dev/null +++ b/module-domain/src/main/java/project/redis/domain/genre/Genre.java @@ -0,0 +1,19 @@ +package project.redis.domain.genre; + +import java.util.UUID; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Value; + +@Getter +@Value +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Genre { + UUID genreId; + String genreName; + + public static Genre generateGenre(UUID genreId, String genreName) { + return new Genre(genreId, genreName); + } +} diff --git a/module-domain/src/main/java/project/redis/domain/movie/Movie.java b/module-domain/src/main/java/project/redis/domain/movie/Movie.java new file mode 100644 index 000000000..9f8fceec2 --- /dev/null +++ b/module-domain/src/main/java/project/redis/domain/movie/Movie.java @@ -0,0 +1,31 @@ +package project.redis.domain.movie; + +import java.time.LocalDate; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Value; +import project.redis.domain.genre.Genre; + +@Getter +@Value +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Movie { + UUID movieId; + String title; + RatingClassification rating; + LocalDate releaseDate; + String thumbnailUrl; + int runningMinTime; + //TODO: 도메인에서 최소정보로 id 데이터만 가지고 있게되니, 결과적으로 디비를 한번 더 찔러야하는 상황이 생긴다... + Genre genre; + + public static Movie generateMovie( + UUID id, String title, + RatingClassification rating, LocalDate releaseDate, + String thumbnailUrl, int runningMinTime, Genre genre + ) { + return new Movie(id, title, rating, releaseDate, thumbnailUrl, runningMinTime, genre); + } +} diff --git a/module-domain/src/main/java/project/redis/domain/movie/RatingClassification.java b/module-domain/src/main/java/project/redis/domain/movie/RatingClassification.java new file mode 100644 index 000000000..daed4e418 --- /dev/null +++ b/module-domain/src/main/java/project/redis/domain/movie/RatingClassification.java @@ -0,0 +1,18 @@ +package project.redis.domain.movie; + +import lombok.Getter; + +@Getter +public enum RatingClassification { + ALL("전체관람가"), + TWELVE("12세 이상 관람가"), + FIFTEEN("15세 이상 관람가"), + NINETEEN("19세 이상 관림가"), + RESTRICT("제한상영가"); + + private final String description; + + RatingClassification(String description) { + this.description = description; + } +} diff --git a/module-domain/src/main/java/project/redis/domain/screening/Screening.java b/module-domain/src/main/java/project/redis/domain/screening/Screening.java new file mode 100644 index 000000000..c1d18ae13 --- /dev/null +++ b/module-domain/src/main/java/project/redis/domain/screening/Screening.java @@ -0,0 +1,34 @@ +package project.redis.domain.screening; + +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Value; +import project.redis.domain.cinema.Cinema; +import project.redis.domain.movie.Movie; + +@Getter +@Value +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Screening { + UUID screeningId; + LocalDateTime screenStartTime; + LocalDateTime screenEndTime; + Movie movie; + Cinema cinema; + + public static Screening generateScreening( + UUID screeningId, + LocalDateTime screenStartTime, LocalDateTime screenEndTime, + Movie movie, Cinema cinema) { + assert screeningId != null; + assert screenStartTime != null; + assert screenEndTime != null; + assert movie != null; + assert cinema != null; + + return new Screening(screeningId, screenStartTime, screenEndTime, movie, cinema); + } +} diff --git a/module-domain/src/main/java/project/redis/domain/seat/Seat.java b/module-domain/src/main/java/project/redis/domain/seat/Seat.java new file mode 100644 index 000000000..9fccfcc67 --- /dev/null +++ b/module-domain/src/main/java/project/redis/domain/seat/Seat.java @@ -0,0 +1,21 @@ +package project.redis.domain.seat; + +import java.util.UUID; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Value; +import project.redis.domain.cinema.Cinema; + +@Getter +@Value +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class Seat { + UUID seatId; + String seatNumber; + Cinema cinema; + + public static Seat generateSeat(UUID seatId, String seatNumber, Cinema cinema) { + return new Seat(seatId, seatNumber, cinema); + } +} diff --git a/module-domain/src/test/java/project/redis/domain/screening/ScreeningTest.java b/module-domain/src/test/java/project/redis/domain/screening/ScreeningTest.java new file mode 100644 index 000000000..cc22f8f46 --- /dev/null +++ b/module-domain/src/test/java/project/redis/domain/screening/ScreeningTest.java @@ -0,0 +1,47 @@ +package project.redis.domain.screening; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.LocalDateTime; +import java.util.UUID; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import project.redis.domain.cinema.Cinema; +import project.redis.domain.movie.Movie; + +class ScreeningTest { + + + @Test + void testGenerateScreening() { + + Movie movieMock = Mockito.mock(Movie.class); + Cinema cinemaMock = Mockito.mock(Cinema.class); + UUID screeningId = UUID.randomUUID(); + LocalDateTime start = LocalDateTime.now(); + LocalDateTime end = start.plusMinutes(100); + + Screening result = Screening.generateScreening(screeningId, start, end, movieMock, cinemaMock); + + assertThat(result.getScreeningId()).isEqualTo(screeningId); + assertThat(result.getScreenStartTime()).isEqualTo(start); + assertThat(result.getScreenEndTime()).isEqualTo(end); + assertThat(result.getMovie()).isEqualTo(movieMock); + assertThat(result.getCinema()).isEqualTo(cinemaMock); + } + + @Test + void testGenerateScreeningException() { + + Movie movieMock = Mockito.mock(Movie.class); + UUID screeningId = UUID.randomUUID(); + LocalDateTime start = LocalDateTime.now(); + LocalDateTime end = start.plusMinutes(100); + + Assertions.assertThrows( + AssertionError.class, + ()-> Screening.generateScreening(screeningId, start, end, movieMock, null)); + } + +} \ No newline at end of file diff --git a/module-infrastructure/README.md b/module-infrastructure/README.md new file mode 100644 index 000000000..036ae4f51 --- /dev/null +++ b/module-infrastructure/README.md @@ -0,0 +1,106 @@ +## [Infrastructure Module] + +### 책임 + +1. **외부 통신 및 기술 로직 담당** + - 데이터베이스, 외부 API, 메시지 큐 등 외부 시스템과의 통신을 처리. + - JPA, Flyway 등 구체적인 기술을 활용하여 데이터를 저장하고 조회. + +2. **Application 계층의 외부 의존성 제거** + - Domain 객체를 활용하여 외부 기술의 의존성을 철저히 제거. + - Application 계층은 Infrastructure 계층의 존재를 알지 못하며, Port-Adapter 패턴을 통해 간접적으로 의존. + +3. **데이터 변환 및 매핑** + - JPA Entity를 Domain 객체로 변환하여 Application 계층에 전달. + - Domain 객체에서 필요한 정보만 추출하여 비즈니스 로직에 활용 가능. + +--- + +### 구조 + +- **Port-Adapter 패턴 적용**: + - Application 계층에서 정의한 `ScreeningQueryPort` 인터페이스를 구현. + - 실제 기술 스택(JPA)을 사용하여 데이터를 처리. + +- **Domain과의 연결**: + - Infrastructure 계층에서 Domain 객체를 참조하여 데이터를 전달. + - 데이터베이스 Entity(`ScreeningJpaEntity`)를 Domain 객체(`Screening`)로 매핑. + +--- + +### 주요 클래스 + +#### **`ScreeningQueryAdapter`** +- Application 계층에서 정의한 `ScreeningQueryPort` 인터페이스를 구현. +- JPA를 사용하여 데이터를 조회하고, Domain 객체로 변환하여 반환. + +```java +@Transactional(readOnly = true) +@Component +@RequiredArgsConstructor +public class ScreeningQueryAdapter implements ScreeningQueryPort { + + private final ScreeningJpaRepository screeningJpaRepository; + + @Override + public List getScreenings(int maxScreeningDay) { + + LocalDate maxScreeningDate = LocalDate.now().plusDays(maxScreeningDay); + + List screenings + = screeningJpaRepository.findAllOrderByReleaseDescAndScreenStartTimeAsc(maxScreeningDate); + + return screenings.stream() + .map(ScreeningInfraMapper::toScreening) + .toList(); + } +} +``` + +--- + +#### **`ScreeningInfraMapper`** +- Entity와 Domain 객체 간의 변환을 처리. + +```java +public class ScreeningInfraMapper { + + public static Screening toScreening(ScreeningJpaEntity entity) { + return Screening.generateScreening( + entity.getScreeningId(), + entity.getScreeningStartTime(), + entity.getScreeningEndTime(), + MovieInfraMapper.toMovie(entity.getMovie()), + CinemaInfraMapper.toCinema(entity.getCinema()) + ); + } + +} +``` + +--- + +### 설계 원칙 + +1. **Port-Adapter 패턴 준수**: + - Port(`ScreeningQueryPort`)를 Application 계층에서 정의하고, Adapter(`ScreeningQueryAdapter`)를 통해 구현. + +2. **기술과 비즈니스의 분리**: + - JPA, Flyway와 같은 기술적인 구현은 Infrastructure 계층에서만 관리. + - Application 계층은 오직 Domain 객체만 활용. + +3. **확장성**: + - 데이터베이스가 변경되거나 다른 기술 스택(NoSQL, 외부 API 등)이 추가되더라도 Port-Adapter 패턴을 통해 쉽게 확장 가능. + +4. **Domain 중심 설계**: + - 데이터를 Domain 객체로 변환하여 Application 계층에 전달. + - 비즈니스 로직은 Application 계층에서 수행. + +--- + +### 장점 +- **유지보수성**: 외부 기술 변경(JPA -> 다른 ORM) 시에도 Application 계층은 수정이 필요 없음. +- **확장성**: 새로운 데이터 소스나 외부 API 추가 시 Port와 Adapter만 추가하면 됨. +- **독립성**: 비즈니스 로직과 기술 로직을 철저히 분리하여 각 계층의 역할을 명확히 함. + +--- diff --git a/module-infrastructure/build.gradle b/module-infrastructure/build.gradle new file mode 100644 index 000000000..47df7050d --- /dev/null +++ b/module-infrastructure/build.gradle @@ -0,0 +1,30 @@ +plugins { + id 'org.springframework.boot' version '3.4.1' +} + +jar { + enabled = true +} + +bootJar { + enabled = false +} + +bootRun { + enabled = false +} + +group = 'project.redis.infrastructure' +version = '0.0.1-SNAPSHOT' + + +dependencies { + implementation project(':module-domain') + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'org.flywaydb:flyway-core' + implementation 'org.flywaydb:flyway-mysql' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/ScreeningDataInit.java b/module-infrastructure/src/main/java/project/redis/infrastructure/ScreeningDataInit.java new file mode 100644 index 000000000..3034ad1c5 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/ScreeningDataInit.java @@ -0,0 +1,70 @@ +//package project.redis.infrastructure; +// +//import java.time.LocalDate; +//import java.time.LocalDateTime; +//import java.time.temporal.ChronoUnit; +//import java.util.List; +//import java.util.Random; +//import java.util.concurrent.ThreadLocalRandom; +//import java.util.stream.Stream; +//import lombok.RequiredArgsConstructor; +//import org.springframework.boot.CommandLineRunner; +//import org.springframework.stereotype.Component; +//import project.redis.infrastructure.cinema.entity.CinemaJpaEntity; +//import project.redis.infrastructure.cinema.repository.CinemaJpaRepository; +//import project.redis.infrastructure.movie.entity.MovieJpaEntity; +//import project.redis.infrastructure.movie.repository.MovieJpaRepository; +//import project.redis.infrastructure.screening.entity.ScreeningJpaEntity; +//import project.redis.infrastructure.screening.repository.ScreeningJpaRepository; +// +//@Component +//@RequiredArgsConstructor +//public class ScreeningDataInit implements CommandLineRunner { +// +// private final ScreeningJpaRepository screeningJpaRepository; +// private final MovieJpaRepository movieJpaRepository; +// private final CinemaJpaRepository cinemaJpaRepository; +// +// private static final Random RANDOM = new Random(); +// +// @Override +// public void run(String... args) throws Exception { +// List movies = movieJpaRepository.findAll(); +// List cinemas = cinemaJpaRepository.findAll(); +// +// Stream.iterate(0, i -> i + 1) +// .limit(500) +// .parallel() +// .map(index -> { +// +// MovieJpaEntity movieJpaEntity = movies.get(RANDOM.nextInt(movies.size())); +// CinemaJpaEntity cinemaJpaEntity = cinemas.get(RANDOM.nextInt(cinemas.size())); +// +// LocalDateTime startTime = generateRandomStartTime(); +// LocalDateTime endTime = startTime.plusMinutes(movieJpaEntity.getRunningMinTime()); +// +// return ScreeningJpaEntity.builder() +// .screeningStartTime(startTime) +// .screeningEndTime(endTime) +// .movie(movieJpaEntity) +// .cinema(cinemaJpaEntity) +// .build(); +// +// }) +// .forEach(screeningJpaRepository::save); +// +// } +// +// public LocalDateTime generateRandomStartTime() { +// LocalDate today = LocalDate.now(); +// LocalDate startDate = today.plusDays(1); +// LocalDate endDate = today.plusDays(20); +// +// long randomDays = ThreadLocalRandom.current() +// .nextLong(ChronoUnit.DAYS.between(startDate, endDate) + 1); +// +// return startDate +// .plusDays(randomDays) +// .atTime(new Random().nextInt(18), new Random().nextInt(60)); +// } +//} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/entity/CinemaJpaEntity.java b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/entity/CinemaJpaEntity.java new file mode 100644 index 000000000..dddb09469 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/entity/CinemaJpaEntity.java @@ -0,0 +1,33 @@ +package project.redis.infrastructure.cinema.entity; + + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.UuidGenerator; +import project.redis.infrastructure.common.entity.BaseJpaEntity; + +@Entity +@Builder +@Table(name = "cinema") +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class CinemaJpaEntity extends BaseJpaEntity { + @Id + @GeneratedValue(generator = "uuid") + @UuidGenerator + @Column(name = "cinema_id", columnDefinition = "BINARY(16)") + private UUID id; + + @Column(nullable = false) + private String cinemaName; +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/inbound/port/CinemaCommandAdapter.java b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/inbound/port/CinemaCommandAdapter.java new file mode 100644 index 000000000..3a539ec00 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/inbound/port/CinemaCommandAdapter.java @@ -0,0 +1,31 @@ +package project.redis.infrastructure.cinema.inbound.port; + + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import project.redis.infrastructure.cinema.entity.CinemaJpaEntity; +import project.redis.infrastructure.cinema.repository.CinemaJpaRepository; + +@Component +@RequiredArgsConstructor +public class CinemaCommandAdapter implements CinemaCommandPort { + + private final CinemaJpaRepository cinemaJpaRepository; + + @Override + public void createCinema(String cinemaName) throws IllegalArgumentException { + Optional cinemaOptional = cinemaJpaRepository.findByCinemaName(cinemaName); + + //TODO: 예외 처리를 담당하는 Exception, ExceptionHandler를 모아두는 모듈 필요 + if (cinemaOptional.isPresent()) { + throw new IllegalArgumentException("Cinema with name '" + cinemaName + "' already exists"); + } + + cinemaJpaRepository.save( + CinemaJpaEntity.builder() + .cinemaName(cinemaName) + .build() + ); + } +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/inbound/port/CinemaCommandPort.java b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/inbound/port/CinemaCommandPort.java new file mode 100644 index 000000000..b7658ba54 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/inbound/port/CinemaCommandPort.java @@ -0,0 +1,6 @@ +package project.redis.infrastructure.cinema.inbound.port; + +public interface CinemaCommandPort { + + void createCinema(String cinemaName) throws IllegalArgumentException; +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/inbound/port/CinemaQueryAdapter.java b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/inbound/port/CinemaQueryAdapter.java new file mode 100644 index 000000000..e1ba05d69 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/inbound/port/CinemaQueryAdapter.java @@ -0,0 +1,25 @@ +package project.redis.infrastructure.cinema.inbound.port; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import project.redis.domain.cinema.Cinema; +import project.redis.infrastructure.cinema.entity.CinemaJpaEntity; +import project.redis.infrastructure.cinema.mapper.CinemaInfraMapper; +import project.redis.infrastructure.cinema.repository.CinemaJpaRepository; + + +@Component +@RequiredArgsConstructor +public class CinemaQueryAdapter implements CinemaQueryPort { + + private final CinemaJpaRepository cinemaJpaRepository; + + @Override + public List getCinemas() { + List cinemas = cinemaJpaRepository.findAll(); + return cinemas.stream() + .map(CinemaInfraMapper::toCinema) + .toList(); + } +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/inbound/port/CinemaQueryPort.java b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/inbound/port/CinemaQueryPort.java new file mode 100644 index 000000000..e70051a56 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/inbound/port/CinemaQueryPort.java @@ -0,0 +1,9 @@ +package project.redis.infrastructure.cinema.inbound.port; + +import java.util.List; +import project.redis.domain.cinema.Cinema; + +public interface CinemaQueryPort { + + List getCinemas(); +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/mapper/CinemaInfraMapper.java b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/mapper/CinemaInfraMapper.java new file mode 100644 index 000000000..1e76781b9 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/mapper/CinemaInfraMapper.java @@ -0,0 +1,16 @@ +package project.redis.infrastructure.cinema.mapper; + + +import project.redis.domain.cinema.Cinema; +import project.redis.infrastructure.cinema.entity.CinemaJpaEntity; + +public class CinemaInfraMapper { + + public static Cinema toCinema(CinemaJpaEntity cinema) { + return Cinema.generateCinema( + cinema.getId(), + cinema.getCinemaName() + ); + } + +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/repository/CinemaJpaRepository.java b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/repository/CinemaJpaRepository.java new file mode 100644 index 000000000..057b46452 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/cinema/repository/CinemaJpaRepository.java @@ -0,0 +1,11 @@ +package project.redis.infrastructure.cinema.repository; + +import java.util.Optional; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import project.redis.infrastructure.cinema.entity.CinemaJpaEntity; + +public interface CinemaJpaRepository extends JpaRepository { + + Optional findByCinemaName(String cinemaName); +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/common/config/JpaConfig.java b/module-infrastructure/src/main/java/project/redis/infrastructure/common/config/JpaConfig.java new file mode 100644 index 000000000..13661c86c --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/common/config/JpaConfig.java @@ -0,0 +1,18 @@ +package project.redis.infrastructure.common.config; + + +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; + +@Configuration +@EnableJpaAuditing +@EnableJpaRepositories(basePackages = { + "project.redis.infrastructure" +}) +@EntityScan(basePackages = { + "project.redis.infrastructure" +}) +public class JpaConfig { +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/common/entity/BaseJpaEntity.java b/module-infrastructure/src/main/java/project/redis/infrastructure/common/entity/BaseJpaEntity.java new file mode 100644 index 000000000..859c2b4e8 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/common/entity/BaseJpaEntity.java @@ -0,0 +1,33 @@ +package project.redis.infrastructure.common.entity; + + +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.Getter; +import org.springframework.data.annotation.CreatedBy; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedBy; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@Getter +@EntityListeners(AuditingEntityListener.class) +@MappedSuperclass +public abstract class BaseJpaEntity { + @CreatedDate + private LocalDateTime createdAt; + + @LastModifiedDate + private LocalDateTime updatedAt; + + @CreatedBy + @Column(updatable = false, columnDefinition = "BINARY(16)") + private UUID createdBy; + + @LastModifiedBy + @Column(columnDefinition = "BINARY(16)") + private UUID updatedBy; +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/genre/entity/GenreJpaEntity.java b/module-infrastructure/src/main/java/project/redis/infrastructure/genre/entity/GenreJpaEntity.java new file mode 100644 index 000000000..50592b666 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/genre/entity/GenreJpaEntity.java @@ -0,0 +1,33 @@ +package project.redis.infrastructure.genre.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.UuidGenerator; +import project.redis.infrastructure.common.entity.BaseJpaEntity; + + +@Entity +@Builder +@Table(name = "genre") +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class GenreJpaEntity extends BaseJpaEntity { + @Id + @GeneratedValue(generator = "uuid") + @UuidGenerator + @Column(name = "genre_id", columnDefinition = "BINARY(16)") + private UUID id; + + @Column(nullable = false) + private String genreName; +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/genre/mapper/GenreInfraMapper.java b/module-infrastructure/src/main/java/project/redis/infrastructure/genre/mapper/GenreInfraMapper.java new file mode 100644 index 000000000..a64c23647 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/genre/mapper/GenreInfraMapper.java @@ -0,0 +1,14 @@ +package project.redis.infrastructure.genre.mapper; + +import project.redis.domain.genre.Genre; +import project.redis.infrastructure.genre.entity.GenreJpaEntity; + +public class GenreInfraMapper { + + public static Genre toGenre(GenreJpaEntity genre) { + return Genre.generateGenre( + genre.getId(), + genre.getGenreName() + ); + } +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/genre/repository/GenreJpaRepository.java b/module-infrastructure/src/main/java/project/redis/infrastructure/genre/repository/GenreJpaRepository.java new file mode 100644 index 000000000..3df16f7d9 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/genre/repository/GenreJpaRepository.java @@ -0,0 +1,8 @@ +package project.redis.infrastructure.genre.repository; + +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import project.redis.infrastructure.genre.entity.GenreJpaEntity; + +public interface GenreJpaRepository extends JpaRepository { +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/movie/entity/MovieJpaEntity.java b/module-infrastructure/src/main/java/project/redis/infrastructure/movie/entity/MovieJpaEntity.java new file mode 100644 index 000000000..f81b34fd1 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/movie/entity/MovieJpaEntity.java @@ -0,0 +1,63 @@ +package project.redis.infrastructure.movie.entity; + + +import static jakarta.persistence.EnumType.STRING; + +import jakarta.persistence.Column; +import jakarta.persistence.ConstraintMode; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import java.time.LocalDate; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.UuidGenerator; +import project.redis.infrastructure.common.entity.BaseJpaEntity; +import project.redis.domain.movie.RatingClassification; +import project.redis.infrastructure.genre.entity.GenreJpaEntity; + +@Entity +@Builder +@Table(name = "movie") +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class MovieJpaEntity extends BaseJpaEntity { + + @Id + @GeneratedValue(generator = "uuid") + @UuidGenerator + @Column(name = "movie_id", columnDefinition = "BINARY(16)") + private UUID id; + + @Column(nullable = false) + private String title; + + @Column(nullable = false) + @Enumerated(value = STRING) + private RatingClassification rating; + + @Column(nullable = false) + private LocalDate releaseDate; + + @Column(columnDefinition = "TEXT") + private String thumbnailUrl; + + @Column(nullable = false) + private int runningMinTime; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "genre_id", columnDefinition = "BINARY(16)", + foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) + private GenreJpaEntity genre; +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/movie/inbound/MovieQueryAdapter.java b/module-infrastructure/src/main/java/project/redis/infrastructure/movie/inbound/MovieQueryAdapter.java new file mode 100644 index 000000000..d7f98af63 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/movie/inbound/MovieQueryAdapter.java @@ -0,0 +1,26 @@ +package project.redis.infrastructure.movie.inbound; + +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import project.redis.domain.movie.Movie; +import project.redis.infrastructure.movie.entity.MovieJpaEntity; +import project.redis.infrastructure.movie.inbound.port.MovieQueryPort; +import project.redis.infrastructure.movie.mapper.MovieInfraMapper; +import project.redis.infrastructure.movie.repository.MovieJpaRepository; + +@Component +@RequiredArgsConstructor +public class MovieQueryAdapter implements MovieQueryPort { + + private final MovieJpaRepository movieJpaRepository; + + @Override + public List getMovies() { + List movies = movieJpaRepository.findAll(); + + return movies.stream() + .map(MovieInfraMapper::toMovie) + .toList(); + } +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/movie/inbound/port/MovieQueryPort.java b/module-infrastructure/src/main/java/project/redis/infrastructure/movie/inbound/port/MovieQueryPort.java new file mode 100644 index 000000000..b1381ad34 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/movie/inbound/port/MovieQueryPort.java @@ -0,0 +1,8 @@ +package project.redis.infrastructure.movie.inbound.port; + +import java.util.List; +import project.redis.domain.movie.Movie; + +public interface MovieQueryPort { + List getMovies(); +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/movie/mapper/MovieInfraMapper.java b/module-infrastructure/src/main/java/project/redis/infrastructure/movie/mapper/MovieInfraMapper.java new file mode 100644 index 000000000..c69756456 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/movie/mapper/MovieInfraMapper.java @@ -0,0 +1,22 @@ +package project.redis.infrastructure.movie.mapper; + + +import project.redis.domain.movie.Movie; +import project.redis.infrastructure.genre.mapper.GenreInfraMapper; +import project.redis.infrastructure.movie.entity.MovieJpaEntity; + +public class MovieInfraMapper { + + public static Movie toMovie(MovieJpaEntity movie) { + return Movie.generateMovie( + movie.getId(), + movie.getTitle(), + movie.getRating(), + movie.getReleaseDate(), + movie.getThumbnailUrl(), + movie.getRunningMinTime(), + GenreInfraMapper.toGenre(movie.getGenre()) + ); + } + +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/movie/repository/MovieJpaRepository.java b/module-infrastructure/src/main/java/project/redis/infrastructure/movie/repository/MovieJpaRepository.java new file mode 100644 index 000000000..3013f0d8f --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/movie/repository/MovieJpaRepository.java @@ -0,0 +1,13 @@ +package project.redis.infrastructure.movie.repository; + +import java.util.List; +import java.util.UUID; +import org.springframework.data.jpa.repository.EntityGraph; +import org.springframework.data.jpa.repository.JpaRepository; +import project.redis.infrastructure.movie.entity.MovieJpaEntity; + +public interface MovieJpaRepository extends JpaRepository { + + @EntityGraph(attributePaths = {"genre"}) + List findAll(); +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/screening/entity/ScreeningJpaEntity.java b/module-infrastructure/src/main/java/project/redis/infrastructure/screening/entity/ScreeningJpaEntity.java new file mode 100644 index 000000000..3003bc50f --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/screening/entity/ScreeningJpaEntity.java @@ -0,0 +1,55 @@ +package project.redis.infrastructure.screening.entity; + + +import jakarta.persistence.Column; +import jakarta.persistence.ConstraintMode; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.time.LocalDateTime; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.UuidGenerator; +import project.redis.infrastructure.cinema.entity.CinemaJpaEntity; +import project.redis.infrastructure.common.entity.BaseJpaEntity; +import project.redis.infrastructure.movie.entity.MovieJpaEntity; + +@Entity +@Builder +@Table(name = "screening") +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class ScreeningJpaEntity extends BaseJpaEntity { + + @Id + @GeneratedValue(generator = "uuid") + @UuidGenerator + @Column(name = "screening_id", columnDefinition = "BINARY(16)") + private UUID id; + + @Column(nullable = false) + private LocalDateTime screeningStartTime; + + @Column(nullable = false) + private LocalDateTime screeningEndTime; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "movie_id", columnDefinition = "BINARY(16)", + foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) + private MovieJpaEntity movie; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "cinema_id", columnDefinition = "BINARY(16)", + foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) + private CinemaJpaEntity cinema; +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/screening/inbound/ScreeningQueryAdapter.java b/module-infrastructure/src/main/java/project/redis/infrastructure/screening/inbound/ScreeningQueryAdapter.java new file mode 100644 index 000000000..6e6ae79c3 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/screening/inbound/ScreeningQueryAdapter.java @@ -0,0 +1,34 @@ +package project.redis.infrastructure.screening.inbound; + +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import project.redis.domain.screening.Screening; +import project.redis.infrastructure.screening.entity.ScreeningJpaEntity; +import project.redis.infrastructure.screening.inbound.port.inbound.ScreeningQueryPort; +import project.redis.infrastructure.screening.mapper.ScreeningInfraMapper; +import project.redis.infrastructure.screening.repository.ScreeningJpaRepository; + + +@Transactional(readOnly = true) +@Component +@RequiredArgsConstructor +public class ScreeningQueryAdapter implements ScreeningQueryPort { + + private final ScreeningJpaRepository screeningJpaRepository; + + @Override + public List getScreenings(int maxScreeningDay) { + + LocalDate maxScreeningDate = LocalDate.now().plusDays(maxScreeningDay); + + List screenings + = screeningJpaRepository.findAllOrderByReleaseDescAndScreenStartTimeAsc(maxScreeningDate); + + return screenings.stream() + .map(ScreeningInfraMapper::toScreening) + .toList(); + } +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/screening/inbound/port/inbound/ScreeningQueryPort.java b/module-infrastructure/src/main/java/project/redis/infrastructure/screening/inbound/port/inbound/ScreeningQueryPort.java new file mode 100644 index 000000000..ab9233196 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/screening/inbound/port/inbound/ScreeningQueryPort.java @@ -0,0 +1,9 @@ +package project.redis.infrastructure.screening.inbound.port.inbound; + +import java.util.List; +import project.redis.domain.screening.Screening; + +public interface ScreeningQueryPort { + + List getScreenings(int maxScreeningDay); +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/screening/mapper/ScreeningInfraMapper.java b/module-infrastructure/src/main/java/project/redis/infrastructure/screening/mapper/ScreeningInfraMapper.java new file mode 100644 index 000000000..90b2b6da5 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/screening/mapper/ScreeningInfraMapper.java @@ -0,0 +1,21 @@ +package project.redis.infrastructure.screening.mapper; + + +import project.redis.domain.screening.Screening; +import project.redis.infrastructure.cinema.mapper.CinemaInfraMapper; +import project.redis.infrastructure.movie.mapper.MovieInfraMapper; +import project.redis.infrastructure.screening.entity.ScreeningJpaEntity; + +public class ScreeningInfraMapper { + + public static Screening toScreening(ScreeningJpaEntity screening) { + return Screening.generateScreening( + screening.getId(), + screening.getScreeningStartTime(), + screening.getScreeningEndTime(), + MovieInfraMapper.toMovie(screening.getMovie()), + CinemaInfraMapper.toCinema(screening.getCinema()) + ); + } + +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/screening/repository/ScreeningJpaRepository.java b/module-infrastructure/src/main/java/project/redis/infrastructure/screening/repository/ScreeningJpaRepository.java new file mode 100644 index 000000000..2eef1c2a1 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/screening/repository/ScreeningJpaRepository.java @@ -0,0 +1,20 @@ +package project.redis.infrastructure.screening.repository; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import project.redis.infrastructure.screening.entity.ScreeningJpaEntity; + +public interface ScreeningJpaRepository extends JpaRepository { + + @Query("select s from ScreeningJpaEntity s " + + "left join fetch s.movie m " + + "left join fetch s.movie.genre g " + + "left join fetch s.cinema c " + + "where date(s.screeningStartTime) BETWEEN current_date AND :limit " + + "order by s.screeningStartTime asc, m.releaseDate desc") + List findAllOrderByReleaseDescAndScreenStartTimeAsc(@Param("limit") LocalDate limit); +} diff --git a/module-infrastructure/src/main/java/project/redis/infrastructure/seat/entity/SeatJpaEntity.java b/module-infrastructure/src/main/java/project/redis/infrastructure/seat/entity/SeatJpaEntity.java new file mode 100644 index 000000000..9839cc365 --- /dev/null +++ b/module-infrastructure/src/main/java/project/redis/infrastructure/seat/entity/SeatJpaEntity.java @@ -0,0 +1,44 @@ +package project.redis.infrastructure.seat.entity; + + +import jakarta.persistence.Column; +import jakarta.persistence.ConstraintMode; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.ForeignKey; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import java.util.UUID; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.UuidGenerator; +import project.redis.infrastructure.cinema.entity.CinemaJpaEntity; +import project.redis.infrastructure.common.entity.BaseJpaEntity; + +@Entity +@Builder +@Table(name = "seat") +@Getter +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class SeatJpaEntity extends BaseJpaEntity { + @Id + @GeneratedValue(generator = "uuid") + @UuidGenerator + @Column(name = "seat_id", columnDefinition = "BINARY(16)") + private UUID id; + + @Column(nullable = false) + private String seatNumber; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "cinema_id", columnDefinition = "BINARY(16)", + foreignKey = @ForeignKey(value = ConstraintMode.NO_CONSTRAINT)) + private CinemaJpaEntity cinema; +} diff --git a/module-infrastructure/src/test/java/project/redis/infrastructure/ScreeningDataInitTest.java b/module-infrastructure/src/test/java/project/redis/infrastructure/ScreeningDataInitTest.java new file mode 100644 index 000000000..f844df8df --- /dev/null +++ b/module-infrastructure/src/test/java/project/redis/infrastructure/ScreeningDataInitTest.java @@ -0,0 +1,33 @@ +package project.redis.infrastructure; + +import java.time.LocalDateTime; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import project.redis.infrastructure.cinema.repository.CinemaJpaRepository; +import project.redis.infrastructure.movie.repository.MovieJpaRepository; +import project.redis.infrastructure.screening.repository.ScreeningJpaRepository; + +@ExtendWith(MockitoExtension.class) +class ScreeningDataInitTest { + + @Mock + ScreeningJpaRepository screeningJpaRepository; + + @Mock + MovieJpaRepository movieJpaRepository; + + @Mock + CinemaJpaRepository cinemaJpaRepository; + + @InjectMocks + private ScreeningDataInit screeningDataInit; + + @Test + void testRandomStartTime() { + LocalDateTime startTime = screeningDataInit.generateRandomStartTime(); + System.out.println("startTime = " + startTime); + } +} \ No newline at end of file diff --git a/module-presentation/README.md b/module-presentation/README.md new file mode 100644 index 000000000..485fa71e7 --- /dev/null +++ b/module-presentation/README.md @@ -0,0 +1,100 @@ +## [Presentation Module] + +### 책임 + +1. **Client의 요청 처리** + - 클라이언트로부터 들어오는 요청을 처리합니다. + - 요청 데이터(`ScreeningsQueryRequest`)를 바탕으로 유효성을 검증하고 필요한 데이터를 추출합니다. + +2. **Application 모듈에 로직 위임** + - 요청 데이터를 기반으로 Application 모듈의 인터페이스(`ScreeningQueryUseCase`)를 호출합니다. + - 비즈니스 로직은 Application 모듈에서 처리하며, Presentation 모듈은 단순히 요청/응답에만 집중합니다. + +3. **적절한 응답 반환** + - Application 모듈에서 반환된 결과를 바탕으로 클라이언트가 원하는 응답 형식으로 변환합니다. + - 예: 영화 데이터를 `GroupedScreeningResponse`로 그룹화하여 반환합니다. + +### 구조 + +#### **Presentation과 Application 모듈의 분리** +- Presentation 모듈은 Application 모듈과 직접적으로 의존하지 않고, **Port-Adapter 패턴**을 통해 통신합니다. + - **Port**: `ScreeningQueryUseCase` 인터페이스로, Application 모듈에서 구현됩니다. + - **Adapter**: Application 모듈의 실제 구현체가 주입됩니다. + +#### **파라미터 설계** +- 클라이언트로부터 받은 요청(`ScreeningsQueryRequest`)은 Presentation 모듈 내에서 처리됩니다. +- Application 모듈과의 통신에는 별도의 파라미터 객체(`ScreeningsQueryParam`)를 사용하여 **Presentation의 변경이 Application에 영향을 미치지 않도록 설계**했습니다. + +### 주요 클래스 + +#### **`ScreeningController`** +- 클라이언트 요청을 처리하고 응답을 반환하는 API 컨트롤러. + +```java +@RestController +@RequestMapping("/api/v1/screenings") +@RequiredArgsConstructor +public class ScreeningController { + + private final ScreeningQueryUseCase screeningQueryUseCase; + + @GetMapping + public ResponseEntity> getScreenings(ScreeningsQueryRequest request) { + + List screenings = screeningQueryUseCase.getScreenings( + ScreeningsQueryParam.builder() + .maxScreeningDay(request.getMaxScreeningDay()) + .build() + ); + + return ResponseEntity.ok(ScreeningAppMapper.toGroupedScreeningResponse(screenings)); + } +} +``` + +#### **`ScreeningsQueryRequest`** +- 클라이언트로부터 전달된 요청 데이터를 캡슐화한 클래스. + +```java +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ScreeningsQueryRequest { + @Builder.Default + private int maxScreeningDay = 2; +} +``` + +#### **`GroupedScreeningResponse`** +- 영화별로 그룹화된 응답 데이터를 제공하는 DTO. + +```java +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GroupedScreeningResponse { + private String movieId; + private String movieTitle; + private String rating; + private LocalDate releaseDate; + private String thumbnailUrl; + private int runningMinTime; + private String genreId; + private String genreName; + private List screenings; + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class ScreeningDetail { + private String screeningId; + private LocalDateTime screeningStartTime; + private LocalDateTime screeningEndTime; + private String cinemaId; + private String cinemaName; + } +} +``` \ No newline at end of file diff --git a/module-presentation/build.gradle b/module-presentation/build.gradle new file mode 100644 index 000000000..da09ce1af --- /dev/null +++ b/module-presentation/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'org.springframework.boot' version '3.4.1' +} + +jar { + enabled = false +} + +bootJar { + enabled = true +} + +bootRun { + enabled = true +} + + +group = 'project.redis.presentation' +version = '0.0.1-SNAPSHOT' + +dependencies { + implementation project(':module-application') + implementation project(':module-domain') + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-web' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} diff --git a/module-presentation/src/main/java/project/redis/presentation/TheaterApplication.java b/module-presentation/src/main/java/project/redis/presentation/TheaterApplication.java new file mode 100644 index 000000000..a6e2f4a9e --- /dev/null +++ b/module-presentation/src/main/java/project/redis/presentation/TheaterApplication.java @@ -0,0 +1,18 @@ +package project.redis.presentation; + + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + + +@SpringBootApplication(scanBasePackages = { + "project.redis.application", + "project.redis.presentation", + "project.redis.infrastructure" +}) + +public class TheaterApplication { + public static void main(String[] args) { + SpringApplication.run(TheaterApplication.class, args); + } +} diff --git a/module-presentation/src/main/java/project/redis/presentation/cinema/controller/CinemaController.java b/module-presentation/src/main/java/project/redis/presentation/cinema/controller/CinemaController.java new file mode 100644 index 000000000..b643cf6fc --- /dev/null +++ b/module-presentation/src/main/java/project/redis/presentation/cinema/controller/CinemaController.java @@ -0,0 +1,51 @@ +package project.redis.presentation.cinema.controller; + + +import jakarta.validation.Valid; +import java.util.List; +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import project.redis.application.cinema.port.inbound.CinemaCommandUseCase; +import project.redis.application.cinema.port.inbound.CinemaCreateCommandParam; +import project.redis.application.cinema.port.inbound.CinemaQueryUseCase; +import project.redis.domain.cinema.Cinema; +import project.redis.presentation.cinema.dto.request.CinemaCreateRequest; +import project.redis.presentation.cinema.dto.response.CinemaResponse; +import project.redis.presentation.cinema.mapper.CinemaApiMapper; + +@RestController +@RequestMapping("/api/v1/cinemas") +@RequiredArgsConstructor +public class CinemaController { + + private final CinemaQueryUseCase cinemaQueryUseCase; + private final CinemaCommandUseCase cinemaCommandUseCase; + + @GetMapping + public ResponseEntity> getCinemas() { + List cinemas = cinemaQueryUseCase.getCinemas(); + + List result = cinemas.stream() + .map(CinemaApiMapper::toCinemaResponse) + .toList(); + + return ResponseEntity.ok(result); + } + + @PostMapping + public ResponseEntity createCinema(@RequestBody @Valid CinemaCreateRequest request) { + cinemaCommandUseCase.createCinema( + CinemaCreateCommandParam.builder() + .CinemaName(request.getCinemaName()) + .build() + ); + + return ResponseEntity.status(HttpStatus.CREATED).build(); + } +} diff --git a/module-presentation/src/main/java/project/redis/presentation/cinema/dto/request/CinemaCreateRequest.java b/module-presentation/src/main/java/project/redis/presentation/cinema/dto/request/CinemaCreateRequest.java new file mode 100644 index 000000000..db7ad33cd --- /dev/null +++ b/module-presentation/src/main/java/project/redis/presentation/cinema/dto/request/CinemaCreateRequest.java @@ -0,0 +1,20 @@ +package project.redis.presentation.cinema.dto.request; + + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class CinemaCreateRequest { + + @NotNull + @NotBlank + private String cinemaName; +} diff --git a/module-presentation/src/main/java/project/redis/presentation/cinema/dto/response/CinemaResponse.java b/module-presentation/src/main/java/project/redis/presentation/cinema/dto/response/CinemaResponse.java new file mode 100644 index 000000000..d7de6b4fb --- /dev/null +++ b/module-presentation/src/main/java/project/redis/presentation/cinema/dto/response/CinemaResponse.java @@ -0,0 +1,17 @@ +package project.redis.presentation.cinema.dto.response; + + +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class CinemaResponse { + private UUID cinemaId; + private String cinemaName; +} diff --git a/module-presentation/src/main/java/project/redis/presentation/cinema/mapper/CinemaApiMapper.java b/module-presentation/src/main/java/project/redis/presentation/cinema/mapper/CinemaApiMapper.java new file mode 100644 index 000000000..0a673cf8f --- /dev/null +++ b/module-presentation/src/main/java/project/redis/presentation/cinema/mapper/CinemaApiMapper.java @@ -0,0 +1,15 @@ +package project.redis.presentation.cinema.mapper; + +import project.redis.domain.cinema.Cinema; +import project.redis.presentation.cinema.dto.response.CinemaResponse; + +public class CinemaApiMapper { + + public static CinemaResponse toCinemaResponse(Cinema cinema) { + return CinemaResponse.builder() + .cinemaId(cinema.getCinemaId()) + .cinemaName(cinema.getCinemaName()) + .build(); + } + +} diff --git a/module-presentation/src/main/java/project/redis/presentation/movie/controller/MovieController.java b/module-presentation/src/main/java/project/redis/presentation/movie/controller/MovieController.java new file mode 100644 index 000000000..605175ed9 --- /dev/null +++ b/module-presentation/src/main/java/project/redis/presentation/movie/controller/MovieController.java @@ -0,0 +1,33 @@ +package project.redis.presentation.movie.controller; + + +import java.util.List; +import lombok.RequiredArgsConstructor; +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; +import project.redis.presentation.movie.dto.response.MovieResponse; +import project.redis.application.movie.port.inbound.MovieQueryUseCase; +import project.redis.domain.movie.Movie; +import project.redis.presentation.movie.mapper.MovieApiMapper; + + +@RestController +@RequestMapping("/api/v1/movies") +@RequiredArgsConstructor +public class MovieController { + + public final MovieQueryUseCase movieQueryUseCase; + + @GetMapping + public ResponseEntity> getMovie() { + List movies = movieQueryUseCase.getMovies(); + + List result = movies.stream() + .map(MovieApiMapper::toMovieResponse) + .toList(); + + return ResponseEntity.ok(result); + } +} diff --git a/module-presentation/src/main/java/project/redis/presentation/movie/dto/response/MovieResponse.java b/module-presentation/src/main/java/project/redis/presentation/movie/dto/response/MovieResponse.java new file mode 100644 index 000000000..9710be6ec --- /dev/null +++ b/module-presentation/src/main/java/project/redis/presentation/movie/dto/response/MovieResponse.java @@ -0,0 +1,24 @@ +package project.redis.presentation.movie.dto.response; + + +import java.time.LocalDate; +import java.util.UUID; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class MovieResponse { + + private UUID movieId; + private String title; + private String rating; + private LocalDate releaseDate; + private String thumbnailUrl; + private int runningTimeMin; + private String genreName; +} diff --git a/module-presentation/src/main/java/project/redis/presentation/movie/mapper/MovieApiMapper.java b/module-presentation/src/main/java/project/redis/presentation/movie/mapper/MovieApiMapper.java new file mode 100644 index 000000000..0b0d719f6 --- /dev/null +++ b/module-presentation/src/main/java/project/redis/presentation/movie/mapper/MovieApiMapper.java @@ -0,0 +1,18 @@ +package project.redis.presentation.movie.mapper; + +import project.redis.domain.movie.Movie; +import project.redis.presentation.movie.dto.response.MovieResponse; + +public class MovieApiMapper { + + public static MovieResponse toMovieResponse(Movie movie) { + return MovieResponse.builder() + .movieId(movie.getMovieId()) + .title(movie.getTitle()) + .rating(movie.getRating().toString()) + .releaseDate(movie.getReleaseDate()) + .runningTimeMin(movie.getRunningMinTime()) + .genreName(movie.getGenre().getGenreName()) + .build(); + } +} diff --git a/module-presentation/src/main/java/project/redis/presentation/screening/controller/ScreeningController.java b/module-presentation/src/main/java/project/redis/presentation/screening/controller/ScreeningController.java new file mode 100644 index 000000000..c26c82eec --- /dev/null +++ b/module-presentation/src/main/java/project/redis/presentation/screening/controller/ScreeningController.java @@ -0,0 +1,35 @@ +package project.redis.presentation.screening.controller; + + +import java.util.List; +import lombok.RequiredArgsConstructor; +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; +import project.redis.application.screening.port.inbound.ScreeningQueryUseCase; +import project.redis.application.screening.port.inbound.ScreeningsQueryParam; +import project.redis.domain.screening.Screening; +import project.redis.presentation.screening.dto.request.ScreeningsQueryRequest; +import project.redis.presentation.screening.dto.response.GroupedScreeningResponse; +import project.redis.presentation.screening.mapper.ScreeningAppMapper; + +@RestController +@RequestMapping("/api/v1/screenings") +@RequiredArgsConstructor +public class ScreeningController { + + private final ScreeningQueryUseCase screeningQueryUseCase; + + @GetMapping + public ResponseEntity> getScreenings(ScreeningsQueryRequest request) { + + List screenings = screeningQueryUseCase.getScreenings( + ScreeningsQueryParam.builder() + .maxScreeningDay(request.getMaxScreeningDay()) + .build() + ); + + return ResponseEntity.ok(ScreeningAppMapper.toGroupedScreeningResponse(screenings)); + } +} diff --git a/module-presentation/src/main/java/project/redis/presentation/screening/dto/request/ScreeningsQueryRequest.java b/module-presentation/src/main/java/project/redis/presentation/screening/dto/request/ScreeningsQueryRequest.java new file mode 100644 index 000000000..e8a9a6be6 --- /dev/null +++ b/module-presentation/src/main/java/project/redis/presentation/screening/dto/request/ScreeningsQueryRequest.java @@ -0,0 +1,15 @@ +package project.redis.presentation.screening.dto.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ScreeningsQueryRequest { + @Builder.Default + private int maxScreeningDay = 2; +} diff --git a/module-presentation/src/main/java/project/redis/presentation/screening/dto/response/GroupedScreeningResponse.java b/module-presentation/src/main/java/project/redis/presentation/screening/dto/response/GroupedScreeningResponse.java new file mode 100644 index 000000000..5f4910af2 --- /dev/null +++ b/module-presentation/src/main/java/project/redis/presentation/screening/dto/response/GroupedScreeningResponse.java @@ -0,0 +1,39 @@ +package project.redis.presentation.screening.dto.response; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class GroupedScreeningResponse { + + private String movieId; + private String movieTitle; + private String rating; + private LocalDate releaseDate; + private String thumbnailUrl; + private int runningMinTime; + private String genreId; + private String genreName; + private List screenings; + + + @Data + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class ScreeningDetail { + private String screeningId; + private LocalDateTime screeningStartTime; + private LocalDateTime screeningEndTime; + private String cinemaId; + private String cinemaName; + } +} diff --git a/module-presentation/src/main/java/project/redis/presentation/screening/mapper/ScreeningAppMapper.java b/module-presentation/src/main/java/project/redis/presentation/screening/mapper/ScreeningAppMapper.java new file mode 100644 index 000000000..959b236d1 --- /dev/null +++ b/module-presentation/src/main/java/project/redis/presentation/screening/mapper/ScreeningAppMapper.java @@ -0,0 +1,52 @@ +package project.redis.presentation.screening.mapper; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.stream.Collectors; +import project.redis.domain.movie.Movie; +import project.redis.domain.screening.Screening; +import project.redis.presentation.screening.dto.response.GroupedScreeningResponse; +import project.redis.presentation.screening.dto.response.GroupedScreeningResponse.ScreeningDetail; + +public class ScreeningAppMapper { + + public static List toGroupedScreeningResponse(List screenings) { + Map> groupedByMovie = screenings.stream() + .collect(Collectors.groupingBy(screening -> screening.getMovie().getMovieId())); + + return groupedByMovie.entrySet().stream() + .sorted(Comparator.comparing(entry -> + entry.getValue().get(0).getMovie().getReleaseDate(), + Comparator.reverseOrder()) + ) + .map(entry -> { + Movie movie = entry.getValue().get(0).getMovie(); + List screeningDetails = entry.getValue().stream() + .map(screening -> + ScreeningDetail.builder() + .screeningId(screening.getScreeningId().toString()) + .screeningStartTime(screening.getScreenStartTime()) + .screeningEndTime(screening.getScreenEndTime()) + .cinemaId(screening.getCinema().getCinemaId().toString()) + .cinemaName(screening.getCinema().getCinemaName()) + .build() + ) + .toList(); + + return GroupedScreeningResponse.builder() + .movieId(movie.getMovieId().toString()) + .movieTitle(movie.getTitle()) + .rating(movie.getRating().toString()) + .releaseDate(movie.getReleaseDate()) + .thumbnailUrl(movie.getThumbnailUrl()) + .runningMinTime(movie.getRunningMinTime()) + .genreId(movie.getGenre().getGenreId().toString()) + .genreName(movie.getGenre().getGenreName()) + .screenings(screeningDetails) + .build(); + }) + .toList(); + } +} diff --git a/module-presentation/src/main/resources/application.yaml b/module-presentation/src/main/resources/application.yaml new file mode 100644 index 000000000..cd7148b9f --- /dev/null +++ b/module-presentation/src/main/resources/application.yaml @@ -0,0 +1,32 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3309/redis-movie?useSSL=false&allowPublicKeyRetrieval=true + username: hongs + password: local1234 + + jpa: + hibernate: + ddl-auto: validate + open-in-view: false + show-sql: true + properties: + hibernate: + format_sql: true + + flyway: + enabled: true + baseline-on-migrate: true + placeholder-replacement: false + locations: classpath:db/migration + +logging: + level: + org: + hibernate: + SQL: info + type: + descriptor: + sql: + info + diff --git a/module-presentation/src/main/resources/db/migration/V1__CreateInitTable.sql b/module-presentation/src/main/resources/db/migration/V1__CreateInitTable.sql new file mode 100644 index 000000000..dff8960ba --- /dev/null +++ b/module-presentation/src/main/resources/db/migration/V1__CreateInitTable.sql @@ -0,0 +1,75 @@ +-- genre 테이블 생성 +create table genre +( + genre_id binary(16) not null primary key, + genre_name varchar(255) not null, + created_at datetime(6) null, + updated_at datetime(6) null, + created_by binary(16) null, + updated_by binary(16) null +) engine = innodb + charset = utf8mb4; + +-- movie 테이블 생성 +create table movie +( + movie_id binary(16) not null primary key, + title varchar(255) not null, + rating varchar(255) not null, + release_date date not null, + thumbnail_url text, + running_min_time int not null, + genre_id binary(16) not null, + created_at datetime(6) null, + updated_at datetime(6) null, + created_by binary(16) null, + updated_by binary(16) null, + constraint fk_movie_genre foreign key (genre_id) references genre (genre_id) +) engine = innodb + charset = utf8mb4; + + +-- cinema 테이블 생성 +create table cinema +( + cinema_id binary(16) not null primary key, + cinema_name varchar(255) not null, + created_at datetime(6) null, + updated_at datetime(6) null, + created_by binary(16) null, + updated_by binary(16) null +) engine = innodb + charset = utf8mb4; + + +-- screening 테이블 생성 +create table screening +( + screening_id binary(16) not null primary key, + screening_start_time datetime(6) not null, + screening_end_time datetime(6) not null, + movie_id binary(16) not null, + cinema_id binary(16) not null, + created_at datetime(6) null, + updated_at datetime(6) null, + created_by binary(16) null, + updated_by binary(16) null, + constraint fk_screening_movie foreign key (movie_id) references movie (movie_id), + constraint fk_screening_cinema foreign key (cinema_id) references cinema (cinema_id) +) engine = innodb + charset = utf8mb4; + + +-- seat 테이블 생성 +create table seat +( + seat_id binary(16) not null primary key, + seat_number varchar(255) not null, + cinema_id binary(16) not null, + created_at datetime(6) null, + updated_at datetime(6) null, + created_by binary(16) null, + updated_by binary(16) null, + constraint fk_seat_cinema foreign key (cinema_id) references cinema (cinema_id) +) engine = innodb + charset = utf8mb4; \ No newline at end of file diff --git a/module-presentation/src/main/resources/db/migration/V2__InitData.sql b/module-presentation/src/main/resources/db/migration/V2__InitData.sql new file mode 100644 index 000000000..ae31805dc --- /dev/null +++ b/module-presentation/src/main/resources/db/migration/V2__InitData.sql @@ -0,0 +1,180 @@ +insert into genre (genre_id, genre_name) +values (uuid_to_bin(uuid()), '액션'), + (uuid_to_bin(uuid()), '코미디'), + (uuid_to_bin(uuid()), '드라마'), + (uuid_to_bin(uuid()), '판타지'), + (uuid_to_bin(uuid()), '로맨스'); + +insert into cinema (cinema_id, cinema_name) +values (uuid_to_bin(uuid()), '스타라이트 상영관'), + (uuid_to_bin(uuid()), '드림씨어터'), + (uuid_to_bin(uuid()), '선셋 극장'), + (uuid_to_bin(uuid()), '루프탑 상영관'), + (uuid_to_bin(uuid()), '클래식 상영관'); + +insert into seat (seat_id, seat_number, cinema_id) +values + -- 루프탑 상영관 좌석 + (uuid_to_bin(uuid()), 'A1', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'A2', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'A3', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'A4', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'A5', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'B1', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'B2', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'B3', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'B4', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'B5', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'C1', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'C2', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'C3', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'C4', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'C5', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'D1', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'D2', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'D3', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'D4', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'D5', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'E1', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'E2', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'E3', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'E4', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + (uuid_to_bin(uuid()), 'E5', (select cinema_id from cinema where cinema_name = '루프탑 상영관')), + + -- 클래식 상영관 좌석 + (uuid_to_bin(uuid()), 'A1', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'A2', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'A3', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'A4', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'A5', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'B1', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'B2', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'B3', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'B4', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'B5', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'C1', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'C2', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'C3', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'C4', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'C5', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'D1', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'D2', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'D3', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'D4', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'D5', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'E1', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'E2', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'E3', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'E4', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + (uuid_to_bin(uuid()), 'E5', (select cinema_id from cinema where cinema_name = '클래식 상영관')), + + (uuid_to_bin(uuid()), 'A1', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'A2', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'A3', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'A4', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'A5', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'B1', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'B2', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'B3', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'B4', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'B5', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'C1', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'C2', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'C3', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'C4', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'C5', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'D1', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'D2', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'D3', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'D4', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'D5', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'E1', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'E2', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'E3', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'E4', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + (uuid_to_bin(uuid()), 'E5', (select cinema_id from cinema where cinema_name = '스타라이트 상영관')), + + (uuid_to_bin(uuid()), 'A1', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'A2', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'A3', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'A4', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'A5', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'B1', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'B2', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'B3', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'B4', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'B5', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'C1', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'C2', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'C3', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'C4', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'C5', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'D1', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'D2', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'D3', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'D4', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'D5', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'E1', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'E2', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'E3', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'E4', (select cinema_id from cinema where cinema_name = '드림씨어터')), + (uuid_to_bin(uuid()), 'E5', (select cinema_id from cinema where cinema_name = '드림씨어터')), + + (uuid_to_bin(uuid()), 'A1', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'A2', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'A3', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'A4', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'A5', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'B1', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'B2', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'B3', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'B4', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'B5', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'C1', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'C2', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'C3', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'C4', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'C5', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'D1', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'D2', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'D3', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'D4', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'D5', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'E1', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'E2', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'E3', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'E4', (select cinema_id from cinema where cinema_name = '선셋 극장')), + (uuid_to_bin(uuid()), 'E5', (select cinema_id from cinema where cinema_name = '선셋 극장')); + + +insert into movie (movie_id, title, rating, release_date, thumbnail_url, running_min_time, genre_id) +values + -- 액션 장르 영화 + (uuid_to_bin(uuid()), '매드 맥스: 분노의 도로', 'NINETEEN', '2015-05-15', 'https://example.com/madmax.jpg', 120, + (select genre_id from genre where genre_name = '액션')), + (uuid_to_bin(uuid()), '다이하드', 'NINETEEN', '1988-07-20', 'https://example.com/diehard.jpg', 131, + (select genre_id from genre where genre_name = '액션')), + + -- 코미디 장르 영화 + (uuid_to_bin(uuid()), '슈퍼배드', 'TWELVE', '2010-07-09', 'https://example.com/despicableme.jpg', 95, + (select genre_id from genre where genre_name = '코미디')), + (uuid_to_bin(uuid()), '트루먼 쇼', 'TWELVE', '1998-06-05', 'https://example.com/trumanshow.jpg', 103, + (select genre_id from genre where genre_name = '코미디')), + + -- 드라마 장르 영화 + (uuid_to_bin(uuid()), '쇼생크 탈출', 'FIFTEEN', '1994-09-23', 'https://example.com/shawshank.jpg', 142, + (select genre_id from genre where genre_name = '드라마')), + (uuid_to_bin(uuid()), '포레스트 검프', 'TWELVE', '1994-07-06', 'https://example.com/forrestgump.jpg', 144, + (select genre_id from genre where genre_name = '드라마')), + + -- 판타지 장르 영화 + (uuid_to_bin(uuid()), '반지의 제왕: 반지 원정대', 'FIFTEEN', '2001-12-19', 'https://example.com/lotr.jpg', 178, + (select genre_id from genre where genre_name = '판타지')), + (uuid_to_bin(uuid()), '해리 포터와 마법사의 돌', 'TWELVE', '2001-11-16', 'https://example.com/harrypotter.jpg', 152, + (select genre_id from genre where genre_name = '판타지')), + + -- 로맨스 장르 영화 + (uuid_to_bin(uuid()), '타이타닉', 'FIFTEEN', '1997-12-19', 'https://example.com/titanic.jpg', 195, + (select genre_id from genre where genre_name = '로맨스')), + (uuid_to_bin(uuid()), '노트북', 'FIFTEEN', '2004-06-25', 'https://example.com/notebook.jpg', 123, + (select genre_id from genre where genre_name = '로맨스')); diff --git a/module-presentation/src/main/resources/db/migration/V3__DropForeignKeyAllTables.sql b/module-presentation/src/main/resources/db/migration/V3__DropForeignKeyAllTables.sql new file mode 100644 index 000000000..b2e4cd447 --- /dev/null +++ b/module-presentation/src/main/resources/db/migration/V3__DropForeignKeyAllTables.sql @@ -0,0 +1,4 @@ +ALTER TABLE movie DROP FOREIGN KEY fk_movie_genre; +ALTER TABLE screening DROP FOREIGN KEY fk_screening_movie; +ALTER TABLE screening DROP FOREIGN KEY fk_screening_cinema; +ALTER TABLE seat DROP FOREIGN KEY fk_seat_cinema; \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 000000000..085be3078 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,6 @@ +rootProject.name = 'redis_1st' +include 'module-application' +include 'module-presentation' +include 'module-infrastructure' +include 'module-domain' +