Compare commits
49 Commits
main
...
feature/im
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39bba383d9 | ||
|
|
db5e8272b6 | ||
|
|
9780a84609 | ||
|
|
7283eeb5d3 | ||
|
|
0521b85dd9 | ||
|
|
b836598be3 | ||
|
|
0173fc92ed | ||
|
|
73cc0241d3 | ||
|
|
720c495b51 | ||
|
|
bd7b761636 | ||
|
|
c9c4201920 | ||
|
|
16eed0cbed | ||
|
|
8d5f179647 | ||
|
|
fca765e9c3 | ||
|
|
3ea3b88b1f | ||
|
|
09cb4a3767 | ||
|
|
648638860c | ||
|
|
6f14c96931 | ||
|
|
e009c2786c | ||
|
|
cb79ec5e57 | ||
|
|
d0af7e4444 | ||
|
|
ab32f435b1 | ||
|
|
a5cc6f2513 | ||
|
|
9ddfc24d2f | ||
|
|
8d9a86df64 | ||
|
|
69fe793afa | ||
|
|
928958dd45 | ||
|
|
a235983475 | ||
|
|
183e61188e | ||
|
|
d5eac621ca | ||
|
|
270edc20b5 | ||
|
|
2b249e3be9 | ||
|
|
0c26555665 | ||
|
|
9f70ee0ed9 | ||
|
|
38365de76c | ||
|
|
b36a58f965 | ||
|
|
2028f3e508 | ||
|
|
4133bcca42 | ||
|
|
c290e54dd3 | ||
|
|
47511c5823 | ||
|
|
52abe07aef | ||
|
|
b50babe5dc | ||
|
|
bf531c59e6 | ||
|
|
3755fba271 | ||
|
|
6305a99c1d | ||
|
|
3d46e96910 | ||
|
|
275cc7f51d | ||
|
|
1145f5d351 | ||
|
|
beecfb1380 |
13
.gitignore
vendored
Normal file
13
.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
### macOS ###
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
### Windows ###
|
||||||
|
# Windows thumbnail cache files
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
9
.idea/Ticketing.iml
generated
Normal file
9
.idea/Ticketing.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/Ticketing.iml" filepath="$PROJECT_DIR$/.idea/Ticketing.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
10
.idea/runConfigurations.xml
generated
Normal file
10
.idea/runConfigurations.xml
generated
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="RunConfigurationProducerService">
|
||||||
|
<option name="ignoredProducers">
|
||||||
|
<set>
|
||||||
|
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
33
.idea/workspace.xml
generated
Normal file
33
.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="2a93e153-a90c-44db-b1da-6c4424693422" name="Changes" comment="" />
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectId" id="29uM8tIDp7UqdPYE3auX216NPaN" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent">
|
||||||
|
<property name="RunOnceActivity.OpenProjectViewOnStart" value="true" />
|
||||||
|
<property name="RunOnceActivity.ShowReadmeOnStart" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="2a93e153-a90c-44db-b1da-6c4424693422" name="Changes" comment="" />
|
||||||
|
<created>1653958362269</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1653958362269</updated>
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
600
GoogleStyle_Custom.xml
Normal file
600
GoogleStyle_Custom.xml
Normal file
@@ -0,0 +1,600 @@
|
|||||||
|
<code_scheme name="GoogleStyle copy" version="173">
|
||||||
|
<option name="AUTODETECT_INDENTS" value="false" />
|
||||||
|
<option name="OTHER_INDENT_OPTIONS">
|
||||||
|
<value>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
<option name="RIGHT_MARGIN" value="240" />
|
||||||
|
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
|
||||||
|
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
|
||||||
|
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||||
|
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||||
|
<option name="SPACE_BEFORE_ARRAY_INITIALIZER_LBRACE" value="true" />
|
||||||
|
<option name="CALL_PARAMETERS_WRAP" value="1" />
|
||||||
|
<option name="METHOD_PARAMETERS_WRAP" value="1" />
|
||||||
|
<option name="EXTENDS_LIST_WRAP" value="1" />
|
||||||
|
<option name="THROWS_KEYWORD_WRAP" value="1" />
|
||||||
|
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
|
||||||
|
<option name="BINARY_OPERATION_WRAP" value="1" />
|
||||||
|
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="TERNARY_OPERATION_WRAP" value="1" />
|
||||||
|
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="FOR_STATEMENT_WRAP" value="1" />
|
||||||
|
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
|
||||||
|
<option name="WRAP_COMMENTS" value="true" />
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
<AndroidXmlCodeStyleSettings>
|
||||||
|
<option name="USE_CUSTOM_SETTINGS" value="true" />
|
||||||
|
<option name="LAYOUT_SETTINGS">
|
||||||
|
<value>
|
||||||
|
<option name="INSERT_BLANK_LINE_BEFORE_TAG" value="false" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</AndroidXmlCodeStyleSettings>
|
||||||
|
<JSCodeStyleSettings version="0">
|
||||||
|
<option name="INDENT_CHAINED_CALLS" value="false" />
|
||||||
|
</JSCodeStyleSettings>
|
||||||
|
<JavaCodeStyleSettings>
|
||||||
|
<option name="INSERT_INNER_CLASS_IMPORTS" value="true" />
|
||||||
|
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
|
||||||
|
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="999" />
|
||||||
|
<option name="PACKAGES_TO_USE_IMPORT_ON_DEMAND">
|
||||||
|
<value />
|
||||||
|
</option>
|
||||||
|
<option name="IMPORT_LAYOUT_TABLE">
|
||||||
|
<value>
|
||||||
|
<package name="" withSubpackages="true" static="true" />
|
||||||
|
<emptyLine />
|
||||||
|
<package name="" withSubpackages="true" static="false" />
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</JavaCodeStyleSettings>
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<Objective-C>
|
||||||
|
<option name="INDENT_NAMESPACE_MEMBERS" value="0" />
|
||||||
|
<option name="INDENT_C_STRUCT_MEMBERS" value="2" />
|
||||||
|
<option name="INDENT_CLASS_MEMBERS" value="2" />
|
||||||
|
<option name="INDENT_VISIBILITY_KEYWORDS" value="1" />
|
||||||
|
<option name="INDENT_INSIDE_CODE_BLOCK" value="2" />
|
||||||
|
<option name="KEEP_STRUCTURES_IN_ONE_LINE" value="true" />
|
||||||
|
<option name="FUNCTION_PARAMETERS_WRAP" value="5" />
|
||||||
|
<option name="FUNCTION_CALL_ARGUMENTS_WRAP" value="5" />
|
||||||
|
<option name="TEMPLATE_CALL_ARGUMENTS_WRAP" value="5" />
|
||||||
|
<option name="TEMPLATE_CALL_ARGUMENTS_ALIGN_MULTILINE" value="true" />
|
||||||
|
<option name="ALIGN_INIT_LIST_IN_COLUMNS" value="false" />
|
||||||
|
<option name="SPACE_BEFORE_SUPERCLASS_COLON" value="false" />
|
||||||
|
</Objective-C>
|
||||||
|
<Objective-C-extensions>
|
||||||
|
<option name="GENERATE_INSTANCE_VARIABLES_FOR_PROPERTIES" value="ASK" />
|
||||||
|
<option name="RELEASE_STYLE" value="IVAR" />
|
||||||
|
<option name="TYPE_QUALIFIERS_PLACEMENT" value="BEFORE" />
|
||||||
|
<file>
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Import" />
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Macro" />
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Typedef" />
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Enum" />
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Constant" />
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Global" />
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Struct" />
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="FunctionPredecl" />
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Function" />
|
||||||
|
</file>
|
||||||
|
<class>
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Property" />
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="Synthesize" />
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InitMethod" />
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="StaticMethod" />
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="InstanceMethod" />
|
||||||
|
<option name="com.jetbrains.cidr.lang.util.OCDeclarationKind" value="DeallocMethod" />
|
||||||
|
</class>
|
||||||
|
<extensions>
|
||||||
|
<pair source="cc" header="h" />
|
||||||
|
<pair source="c" header="h" />
|
||||||
|
</extensions>
|
||||||
|
</Objective-C-extensions>
|
||||||
|
<Python>
|
||||||
|
<option name="USE_CONTINUATION_INDENT_FOR_ARGUMENTS" value="true" />
|
||||||
|
</Python>
|
||||||
|
<TypeScriptCodeStyleSettings version="0">
|
||||||
|
<option name="INDENT_CHAINED_CALLS" value="false" />
|
||||||
|
</TypeScriptCodeStyleSettings>
|
||||||
|
<XML>
|
||||||
|
<option name="XML_ALIGN_ATTRIBUTES" value="false" />
|
||||||
|
</XML>
|
||||||
|
<codeStyleSettings language="CSS">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="ECMA Script Level 4">
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||||
|
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||||
|
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||||
|
<option name="CALL_PARAMETERS_WRAP" value="1" />
|
||||||
|
<option name="METHOD_PARAMETERS_WRAP" value="1" />
|
||||||
|
<option name="EXTENDS_LIST_WRAP" value="1" />
|
||||||
|
<option name="BINARY_OPERATION_WRAP" value="1" />
|
||||||
|
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="TERNARY_OPERATION_WRAP" value="1" />
|
||||||
|
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="FOR_STATEMENT_WRAP" value="1" />
|
||||||
|
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
<option name="PARENT_SETTINGS_INSTALLED" value="true" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="HTML">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="JAVA">
|
||||||
|
<option name="KEEP_CONTROL_STATEMENT_IN_ONE_LINE" value="false" />
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||||
|
<option name="BLANK_LINES_AFTER_CLASS_HEADER" value="1" />
|
||||||
|
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||||
|
<option name="ALIGN_MULTILINE_RESOURCES" value="false" />
|
||||||
|
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||||
|
<option name="CALL_PARAMETERS_WRAP" value="1" />
|
||||||
|
<option name="METHOD_PARAMETERS_WRAP" value="1" />
|
||||||
|
<option name="EXTENDS_LIST_WRAP" value="1" />
|
||||||
|
<option name="THROWS_KEYWORD_WRAP" value="1" />
|
||||||
|
<option name="METHOD_CALL_CHAIN_WRAP" value="1" />
|
||||||
|
<option name="BINARY_OPERATION_WRAP" value="1" />
|
||||||
|
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="TERNARY_OPERATION_WRAP" value="1" />
|
||||||
|
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="FOR_STATEMENT_WRAP" value="1" />
|
||||||
|
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
|
||||||
|
<option name="WRAP_COMMENTS" value="true" />
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="JSON">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="4" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="JSP">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="JavaScript">
|
||||||
|
<option name="RIGHT_MARGIN" value="80" />
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||||
|
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||||
|
<option name="ALIGN_MULTILINE_FOR" value="false" />
|
||||||
|
<option name="CALL_PARAMETERS_WRAP" value="1" />
|
||||||
|
<option name="METHOD_PARAMETERS_WRAP" value="1" />
|
||||||
|
<option name="BINARY_OPERATION_WRAP" value="1" />
|
||||||
|
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="TERNARY_OPERATION_WRAP" value="1" />
|
||||||
|
<option name="TERNARY_OPERATION_SIGNS_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="FOR_STATEMENT_WRAP" value="1" />
|
||||||
|
<option name="ARRAY_INITIALIZER_WRAP" value="1" />
|
||||||
|
<option name="IF_BRACE_FORCE" value="3" />
|
||||||
|
<option name="DOWHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="WHILE_BRACE_FORCE" value="3" />
|
||||||
|
<option name="FOR_BRACE_FORCE" value="3" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="ObjectiveC">
|
||||||
|
<option name="RIGHT_MARGIN" value="80" />
|
||||||
|
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="1" />
|
||||||
|
<option name="BLANK_LINES_BEFORE_IMPORTS" value="0" />
|
||||||
|
<option name="BLANK_LINES_AFTER_IMPORTS" value="0" />
|
||||||
|
<option name="BLANK_LINES_AROUND_CLASS" value="0" />
|
||||||
|
<option name="BLANK_LINES_AROUND_METHOD" value="0" />
|
||||||
|
<option name="BLANK_LINES_AROUND_METHOD_IN_INTERFACE" value="0" />
|
||||||
|
<option name="ALIGN_MULTILINE_BINARY_OPERATION" value="false" />
|
||||||
|
<option name="BINARY_OPERATION_SIGN_ON_NEXT_LINE" value="true" />
|
||||||
|
<option name="FOR_STATEMENT_WRAP" value="1" />
|
||||||
|
<option name="ASSIGNMENT_WRAP" value="1" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="PROTO">
|
||||||
|
<option name="RIGHT_MARGIN" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="Python">
|
||||||
|
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||||
|
<option name="RIGHT_MARGIN" value="80" />
|
||||||
|
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||||
|
<option name="PARENT_SETTINGS_INSTALLED" value="true" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="SASS">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="SCSS">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="TypeScript">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<indentOptions>
|
||||||
|
<option name="INDENT_SIZE" value="2" />
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
<option name="TAB_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:.*Style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_width</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_height</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_weight</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_margin</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_marginTop</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_marginBottom</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_marginStart</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_marginEnd</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_marginLeft</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_marginRight</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:layout_.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:padding</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:paddingTop</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:paddingBottom</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:paddingStart</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:paddingEnd</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:paddingLeft</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:paddingRight</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res-auto</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/tools</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="USE_TAB_CHARACTER" value="true" />
|
||||||
|
<option name="SMART_TABS" value="true" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="protobuf">
|
||||||
|
<option name="RIGHT_MARGIN" value="80" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="2" />
|
||||||
|
</indentOptions>
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
118
README.md
118
README.md
@@ -1,5 +1,5 @@
|
|||||||
# 🍿 Ticketing
|
# 🍿 Ticketing
|
||||||
영화 예매 서비스
|
영화 예매 사이트를 대용량 트래픽에 대응할 수 있도록 설계.
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
@@ -8,10 +8,120 @@
|
|||||||
- 기획 및 설계 : 22.04.13 ~
|
- 기획 및 설계 : 22.04.13 ~
|
||||||
- 프로젝트 구현 :
|
- 프로젝트 구현 :
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
<br/><br/>
|
## 🎯 프로젝트 주요 관심사
|
||||||
|
- OOP(객체 지향 프로그래밍)의 장점을 최대한 활용
|
||||||
|
- 테스트 코드를 통한 믿을 수 있는 코드 작성
|
||||||
|
- 대용량 트래픽을 고려한 확장 가능한 설계 및 성능 튜닝
|
||||||
|
- 코드리뷰를 통해 코드 품질 향상
|
||||||
|
- 코드 컨벤션 준수하여 코드 통일성 유지
|
||||||
|
|
||||||
## 🎯 프로젝트 목표
|
<br/>
|
||||||
|
|
||||||
|
## 🛠 기술스택
|
||||||
|
- Java 11
|
||||||
|
- Spring Boot 2.6.7 (당시 최신 GA 버전)
|
||||||
|
- Gradle Kotlin DSL
|
||||||
|
- Spring Security
|
||||||
|
- Junit 5
|
||||||
|
- Hibernate / SpringJPA
|
||||||
|
- MySQL 8.0
|
||||||
|
- Redis
|
||||||
|
|
||||||
<br/><br/>
|
<br/>
|
||||||
|
|
||||||
|
## Wiki
|
||||||
|
- [Git Branch 전략](https://github.com/f-lab-edu/Ticketing/wiki#-git-branch-%EC%A0%84%EB%9E%B5)
|
||||||
|
- [Code Convention](https://github.com/f-lab-edu/Ticketing/wiki#-code-convention)
|
||||||
|
- [패키지 구조](https://github.com/f-lab-edu/Ticketing/wiki#-%ED%8C%A8%ED%82%A4%EC%A7%80-%EA%B5%AC%EC%A1%B0)
|
||||||
|
- [Use Case](https://github.com/f-lab-edu/Ticketing/wiki/Use-Case)
|
||||||
|
- [Prototype](https://github.com/f-lab-edu/Ticketing/wiki/Prototype)
|
||||||
|
- [Issue Posting](https://github.com/f-lab-edu/Ticketing/wiki/Issue-Posting)
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
## 🧩 ERD
|
||||||
|
|
||||||
|
``` mermaid
|
||||||
|
erDiagram
|
||||||
|
MOVIE ||--o{ MOVIE_TIME : ""
|
||||||
|
MOVIE {
|
||||||
|
bigint id PK "영화 ID"
|
||||||
|
varchar title "영화제목"
|
||||||
|
int running_time "러닝타임"
|
||||||
|
datetime deleted_at "삭제일시"
|
||||||
|
datetime created_at "등록일시"
|
||||||
|
datetime updated_at "수정일시"
|
||||||
|
}
|
||||||
|
THEATER ||--o{ MOVIE_TIME : ""
|
||||||
|
THEATER ||--|{ SEAT : ""
|
||||||
|
THEATER {
|
||||||
|
bigint id PK "상영관 ID"
|
||||||
|
int theater_number "상영관 번호"
|
||||||
|
int seat_count "좌석수"
|
||||||
|
datetime deleted_at "삭제일시"
|
||||||
|
datetime created_at "등록일시"
|
||||||
|
datetime updated_at "수정일시"
|
||||||
|
}
|
||||||
|
SEAT ||--o{ TICKET : ""
|
||||||
|
SEAT {
|
||||||
|
bigint id PK "좌석 ID"
|
||||||
|
bigint theater_id FK "상영관 ID"
|
||||||
|
int column "열"
|
||||||
|
int row "행"
|
||||||
|
datetime deleted_at "삭제일시"
|
||||||
|
datetime created_at "등록일시"
|
||||||
|
datetime updated_at "수정일시"
|
||||||
|
}
|
||||||
|
MOVIE_TIME ||--o{ TICKET : ""
|
||||||
|
MOVIE_TIME {
|
||||||
|
bigint id PK "상영시간표 ID"
|
||||||
|
bigint movie_id FK "영화 ID"
|
||||||
|
bigint theater_id FK "상영관 ID"
|
||||||
|
int round "회차"
|
||||||
|
time start_at "시작 시간"
|
||||||
|
time end_at "종료 시간"
|
||||||
|
datetime deleted_at "삭제일시"
|
||||||
|
datetime created_at "등록일시"
|
||||||
|
datetime updated_at "수정일시"
|
||||||
|
}
|
||||||
|
TICKET {
|
||||||
|
bigint id PK "티켓 ID"
|
||||||
|
bigint seat_id FK "좌석 ID"
|
||||||
|
bigint movie_time_id FK "상영시간표 ID"
|
||||||
|
bigint payment_id "결제 ID"
|
||||||
|
varchar status "상태 - 구매가능/예약진행중/판매완료"
|
||||||
|
int ticket_price "가격"
|
||||||
|
datetime deleted_at "삭제일시"
|
||||||
|
datetime created_at "등록일시"
|
||||||
|
datetime updated_at "수정일시"
|
||||||
|
}
|
||||||
|
TICKET }|--|| PAYMENT : ""
|
||||||
|
PAYMENT {
|
||||||
|
bigint id PK "결제 ID"
|
||||||
|
bigint user_alternate_id "유저 대체ID"
|
||||||
|
varchar movie_title "영화제목"
|
||||||
|
varchar type "결제 타입 - 예) 네이버페이, 카카오페이"
|
||||||
|
varchar status "상태 - 완료/환불/실패"
|
||||||
|
varchar failed_message "실패사유 - 컬럼명을 알아보기 쉬운가?"
|
||||||
|
varchar payment_number "예매번호"
|
||||||
|
int total_price "결제 금액"
|
||||||
|
datetime deleted_at "삭제일시"
|
||||||
|
datetime created_at "결제일시"
|
||||||
|
datetime updated_at "수정일시"
|
||||||
|
}
|
||||||
|
USER ||--o{ PAYMENT : ""
|
||||||
|
USER {
|
||||||
|
bigint id "회원"
|
||||||
|
bigint alternate_id "대체ID"
|
||||||
|
varchar name "이름"
|
||||||
|
varchar email "이메일"
|
||||||
|
varchar password "비밀번호"
|
||||||
|
varchar grade "등급 - 고객/임직원"
|
||||||
|
varchar phone "휴대폰 번호"
|
||||||
|
datetime deleted_at "탈퇴일시"
|
||||||
|
datetime created_at "가입일시"
|
||||||
|
datetime updated_at "수정일시"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
## 작업 내용
|
## 작업 분류
|
||||||
- [ ] 기능 추가
|
- [ ] 기능 추가
|
||||||
- [ ] 코드 수정
|
- [ ] 코드 수정
|
||||||
- [ ] 환경 설정
|
- [ ] 환경 설정
|
||||||
@@ -30,4 +30,3 @@
|
|||||||
- [ ] 구현 사항에 대한 테스트를 완료했습니다.
|
- [ ] 구현 사항에 대한 테스트를 완료했습니다.
|
||||||
- [ ] 의도한 내용 이외에 다른 코드에는 변경 사항이 없는지 확인했습니다.
|
- [ ] 의도한 내용 이외에 다른 코드에는 변경 사항이 없는지 확인했습니다.
|
||||||
- [ ] 코드 스타일을 적용하여 팀 코딩 컨벤션에 맞게 작성했습니다.
|
- [ ] 코드 스타일을 적용하여 팀 코딩 컨벤션에 맞게 작성했습니다.
|
||||||
- [ ] 팀원 모두 구현 내용 및 코드에 대해 이해하고 있습니다.
|
|
||||||
206
server/.gitignore
vendored
Normal file
206
server/.gitignore
vendored
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/macos,windows,intellij+all,gradle,visualstudiocode
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=macos,windows,intellij+all,gradle,visualstudiocode
|
||||||
|
|
||||||
|
### Intellij+all ###
|
||||||
|
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||||
|
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||||
|
|
||||||
|
HELP.md
|
||||||
|
|
||||||
|
# User-specific stuff
|
||||||
|
.idea/**/workspace.xml
|
||||||
|
.idea/**/tasks.xml
|
||||||
|
.idea/**/usage.statistics.xml
|
||||||
|
.idea/**/dictionaries
|
||||||
|
.idea/**/shelf
|
||||||
|
|
||||||
|
# AWS User-specific
|
||||||
|
.idea/**/aws.xml
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
.idea/**/contentModel.xml
|
||||||
|
|
||||||
|
# Sensitive or high-churn files
|
||||||
|
.idea/**/dataSources/
|
||||||
|
.idea/**/dataSources.ids
|
||||||
|
.idea/**/dataSources.local.xml
|
||||||
|
.idea/**/sqlDataSources.xml
|
||||||
|
.idea/**/dynamic.xml
|
||||||
|
.idea/**/uiDesigner.xml
|
||||||
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Gradle
|
||||||
|
.idea/**/gradle.xml
|
||||||
|
.idea/**/libraries
|
||||||
|
|
||||||
|
# Gradle and Maven with auto-import
|
||||||
|
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||||
|
# since they will be recreated, and may cause churn. Uncomment if using
|
||||||
|
# auto-import.
|
||||||
|
# .idea/artifacts
|
||||||
|
# .idea/compiler.xml
|
||||||
|
# .idea/jarRepositories.xml
|
||||||
|
# .idea/modules.xml
|
||||||
|
# .idea/*.iml
|
||||||
|
# .idea/modules
|
||||||
|
# *.iml
|
||||||
|
# *.ipr
|
||||||
|
|
||||||
|
# CMake
|
||||||
|
cmake-build-*/
|
||||||
|
|
||||||
|
# Mongo Explorer plugin
|
||||||
|
.idea/**/mongoSettings.xml
|
||||||
|
|
||||||
|
# File-based project format
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
out/
|
||||||
|
|
||||||
|
# mpeltonen/sbt-idea plugin
|
||||||
|
.idea_modules/
|
||||||
|
|
||||||
|
# JIRA plugin
|
||||||
|
atlassian-ide-plugin.xml
|
||||||
|
|
||||||
|
# Cursive Clojure plugin
|
||||||
|
.idea/replstate.xml
|
||||||
|
|
||||||
|
# SonarLint plugin
|
||||||
|
.idea/sonarlint/
|
||||||
|
|
||||||
|
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||||
|
com_crashlytics_export_strings.xml
|
||||||
|
crashlytics.properties
|
||||||
|
crashlytics-build.properties
|
||||||
|
fabric.properties
|
||||||
|
|
||||||
|
# Editor-based Rest Client
|
||||||
|
.idea/httpRequests
|
||||||
|
|
||||||
|
# Android studio 3.1+ serialized cache file
|
||||||
|
.idea/caches/build_file_checksums.ser
|
||||||
|
|
||||||
|
### Intellij+all Patch ###
|
||||||
|
# Ignore everything but code style settings and run configurations
|
||||||
|
# that are supposed to be shared within teams.
|
||||||
|
|
||||||
|
.idea/
|
||||||
|
.idea/*.iml
|
||||||
|
.idea/*.xml
|
||||||
|
|
||||||
|
!.idea/codeStyles
|
||||||
|
!.idea/runConfigurations
|
||||||
|
|
||||||
|
### macOS ###
|
||||||
|
# General
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Icon must end with two \r
|
||||||
|
Icon
|
||||||
|
|
||||||
|
|
||||||
|
# Thumbnails
|
||||||
|
._*
|
||||||
|
|
||||||
|
# Files that might appear in the root of a volume
|
||||||
|
.DocumentRevisions-V100
|
||||||
|
.fseventsd
|
||||||
|
.Spotlight-V100
|
||||||
|
.TemporaryItems
|
||||||
|
.Trashes
|
||||||
|
.VolumeIcon.icns
|
||||||
|
.com.apple.timemachine.donotpresent
|
||||||
|
|
||||||
|
# Directories potentially created on remote AFP share
|
||||||
|
.AppleDB
|
||||||
|
.AppleDesktop
|
||||||
|
Network Trash Folder
|
||||||
|
Temporary Items
|
||||||
|
.apdisk
|
||||||
|
|
||||||
|
### macOS Patch ###
|
||||||
|
# iCloud generated files
|
||||||
|
*.icloud
|
||||||
|
|
||||||
|
### VisualStudioCode ###
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# Local History for Visual Studio Code
|
||||||
|
.history/
|
||||||
|
|
||||||
|
# Built Visual Studio Code Extensions
|
||||||
|
*.vsix
|
||||||
|
|
||||||
|
### VisualStudioCode Patch ###
|
||||||
|
# Ignore all local history of files
|
||||||
|
.history
|
||||||
|
.ionide
|
||||||
|
|
||||||
|
# Support for Project snippet scope
|
||||||
|
.vscode/*.code-snippets
|
||||||
|
|
||||||
|
# Ignore code-workspaces
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
|
### Windows ###
|
||||||
|
# Windows thumbnail cache files
|
||||||
|
Thumbs.db
|
||||||
|
Thumbs.db:encryptable
|
||||||
|
ehthumbs.db
|
||||||
|
ehthumbs_vista.db
|
||||||
|
|
||||||
|
# Dump file
|
||||||
|
*.stackdump
|
||||||
|
|
||||||
|
# Folder config file
|
||||||
|
[Dd]esktop.ini
|
||||||
|
|
||||||
|
# Recycle Bin used on file shares
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
|
||||||
|
# Windows Installer files
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msix
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
# Windows shortcuts
|
||||||
|
*.lnk
|
||||||
|
|
||||||
|
### Gradle ###
|
||||||
|
.gradle
|
||||||
|
**/build/
|
||||||
|
!src/**/build/
|
||||||
|
|
||||||
|
# Ignore Gradle GUI config
|
||||||
|
gradle-app.setting
|
||||||
|
|
||||||
|
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
|
||||||
|
!gradle-wrapper.jar
|
||||||
|
|
||||||
|
# Avoid ignore Gradle wrappper properties
|
||||||
|
!gradle-wrapper.properties
|
||||||
|
|
||||||
|
# Cache of project
|
||||||
|
.gradletasknamecache
|
||||||
|
|
||||||
|
# Eclipse Gradle plugin generated files
|
||||||
|
# Eclipse Core
|
||||||
|
.project
|
||||||
|
# JDT-specific (Eclipse Java Development Tools)
|
||||||
|
.classpath
|
||||||
|
|
||||||
|
# log
|
||||||
|
/logs
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/macos,windows,intellij+all,gradle,visualstudiocode
|
||||||
88
server/build.gradle.kts
Normal file
88
server/build.gradle.kts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
plugins {
|
||||||
|
java
|
||||||
|
id("org.springframework.boot") version "2.6.7"
|
||||||
|
id("io.spring.dependency-management") version "1.0.11.RELEASE"
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "com.ticketing"
|
||||||
|
version = "0.0.1-SNAPSHOT"
|
||||||
|
|
||||||
|
val javaVersion = JavaVersion.VERSION_11
|
||||||
|
java {
|
||||||
|
sourceCompatibility = javaVersion
|
||||||
|
targetCompatibility = javaVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations {
|
||||||
|
compileOnly {
|
||||||
|
extendsFrom(configurations.annotationProcessor.get())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-validation")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-log4j2")
|
||||||
|
implementation("org.projectlombok:lombok:1.18.24")
|
||||||
|
implementation("io.springfox:springfox-boot-starter:3.0.0")
|
||||||
|
implementation("io.springfox:springfox-swagger-ui:3.0.0")
|
||||||
|
implementation("com.github.ulisesbocchio:jasypt-spring-boot-starter:3.0.4")
|
||||||
|
implementation("com.lmax:disruptor:3.4.4")
|
||||||
|
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
|
||||||
|
implementation("com.googlecode.json-simple:json-simple:1.1.1")
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-data-redis")
|
||||||
|
implementation("com.google.code.findbugs:jsr305:3.0.2")
|
||||||
|
|
||||||
|
modules {
|
||||||
|
module("org.springframework.boot:spring-boot-starter-logging") {
|
||||||
|
replacedBy("org.springframework.boot:spring-boot-starter-log4j2", "Use Log4j2 instead of Logback")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOnly("org.projectlombok:lombok")
|
||||||
|
runtimeOnly("mysql:mysql-connector-java")
|
||||||
|
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
|
||||||
|
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")
|
||||||
|
annotationProcessor("org.projectlombok:lombok")
|
||||||
|
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
|
||||||
|
|
||||||
|
testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1")
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
testImplementation("org.springframework.security:spring-security-test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.withType<Test> {
|
||||||
|
useJUnitPlatform()
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceSets {
|
||||||
|
create("intTest") {
|
||||||
|
compileClasspath += sourceSets.main.get().output
|
||||||
|
runtimeClasspath += sourceSets.main.get().output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val intTestImplementation by configurations.getting {
|
||||||
|
extendsFrom(configurations.implementation.get())
|
||||||
|
}
|
||||||
|
|
||||||
|
configurations["intTestRuntimeOnly"].extendsFrom(configurations.runtimeOnly.get())
|
||||||
|
configurations["intTestImplementation"].extendsFrom(configurations.testImplementation.get())
|
||||||
|
|
||||||
|
val integrationTest = task<Test>("integrationTest") {
|
||||||
|
description = "Runs integration tests."
|
||||||
|
group = "verification"
|
||||||
|
|
||||||
|
testClassesDirs = sourceSets["intTest"].output.classesDirs
|
||||||
|
classpath = sourceSets["intTest"].runtimeClasspath
|
||||||
|
shouldRunAfter("test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.check { dependsOn(integrationTest) }
|
||||||
BIN
server/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
server/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
5
server/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
5
server/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.1-bin.zip
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
234
server/gradlew
vendored
Executable file
234
server/gradlew
vendored
Executable file
@@ -0,0 +1,234 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||||
|
|
||||||
|
APP_NAME="Gradle"
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
|
||||||
|
# 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"'
|
||||||
|
|
||||||
|
# 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
|
||||||
|
which java >/dev/null 2>&1 || 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
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
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
|
||||||
|
|
||||||
|
# Collect all arguments for the java command;
|
||||||
|
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||||
|
# shell script including quotes and variable substitutions, so put them in
|
||||||
|
# double quotes to make sure that they get re-expanded; and
|
||||||
|
# * put everything else in single quotes, so that it's not re-expanded.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# 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" "$@"
|
||||||
89
server/gradlew.bat
vendored
Normal file
89
server/gradlew.bat
vendored
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%" == "" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%" == "" set DIRNAME=.
|
||||||
|
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%" == "0" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||||
|
echo.
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
echo location of your Java installation.
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if "%ERRORLEVEL%"=="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!
|
||||||
|
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||||
|
exit /b 1
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
1
server/settings.gradle.kts
Normal file
1
server/settings.gradle.kts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
rootProject.name = "server"
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.ticketing.server.global.health;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||||
|
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
||||||
|
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||||
|
class L7checkControllerTest {
|
||||||
|
|
||||||
|
private static final String L7CHECK = "/l7check";
|
||||||
|
private static final String HEALTH = "/actuator/health";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TestRestTemplate restTemplate;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void downAndUp() throws InterruptedException {
|
||||||
|
// before
|
||||||
|
expectUrlStatus(L7CHECK, HttpStatus.OK);
|
||||||
|
expectUrlStatus(HEALTH, HttpStatus.OK);
|
||||||
|
|
||||||
|
// down
|
||||||
|
restTemplate.delete(L7CHECK);
|
||||||
|
|
||||||
|
// then down
|
||||||
|
TimeUnit.MILLISECONDS.sleep(1000);
|
||||||
|
expectUrlStatus(L7CHECK, HttpStatus.SERVICE_UNAVAILABLE);
|
||||||
|
expectUrlStatus(HEALTH, HttpStatus.SERVICE_UNAVAILABLE);
|
||||||
|
|
||||||
|
// up
|
||||||
|
restTemplate.postForEntity(L7CHECK, null, Object.class);
|
||||||
|
|
||||||
|
// then up
|
||||||
|
TimeUnit.MILLISECONDS.sleep(1000);
|
||||||
|
expectUrlStatus(L7CHECK, HttpStatus.OK);
|
||||||
|
expectUrlStatus(HEALTH, HttpStatus.OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expectUrlStatus(String url, HttpStatus status) {
|
||||||
|
ResponseEntity<Object> res = restTemplate.getForEntity(url, Object.class);
|
||||||
|
assertThat(res.getStatusCode()).isEqualTo(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.ticketing.server.global.redis;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertAll;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
class RefreshRedisRepositoryTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RefreshRedisRepository refreshRedisRepository;
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
refreshRedisRepository.deleteAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("기본 등록 및 조회기능")
|
||||||
|
void saveAndFind() {
|
||||||
|
// given
|
||||||
|
RefreshToken refreshToken = new RefreshToken("ticketing@gmail.com", "refreshToken");
|
||||||
|
|
||||||
|
// when
|
||||||
|
refreshRedisRepository.save(refreshToken);
|
||||||
|
|
||||||
|
// then
|
||||||
|
RefreshToken findRefreshToken = refreshRedisRepository.findById(refreshToken.getId()).get();
|
||||||
|
assertAll(
|
||||||
|
() -> assertThat(findRefreshToken.getEmail()).isEqualTo("ticketing@gmail.com")
|
||||||
|
, () -> assertThat(findRefreshToken.getToken()).isEqualTo("refreshToken")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("기본 등록 및 이메일 조회")
|
||||||
|
void saveAndFindByEmail() {
|
||||||
|
// given
|
||||||
|
RefreshToken refreshToken = new RefreshToken("ticketing@gmail.com", "refreshToken");
|
||||||
|
|
||||||
|
// when
|
||||||
|
refreshRedisRepository.save(refreshToken);
|
||||||
|
|
||||||
|
// then
|
||||||
|
RefreshToken findRefreshToken = refreshRedisRepository.findByEmail(refreshToken.getEmail()).get();
|
||||||
|
assertAll(
|
||||||
|
() -> assertThat(findRefreshToken.getEmail()).isEqualTo("ticketing@gmail.com")
|
||||||
|
, () -> assertThat(findRefreshToken.getToken()).isEqualTo("refreshToken")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("기본 등록 및 수정기능")
|
||||||
|
void saveAndSave() {
|
||||||
|
// given
|
||||||
|
RefreshToken refreshToken = new RefreshToken("ticketing@gmail.com", "refreshToken");
|
||||||
|
refreshRedisRepository.save(refreshToken);
|
||||||
|
Long id = refreshToken.getId();
|
||||||
|
|
||||||
|
// when
|
||||||
|
RefreshToken savedRefreshToken = refreshRedisRepository.findById(id).get();
|
||||||
|
savedRefreshToken.changeToken("refreshToken2");
|
||||||
|
refreshRedisRepository.save(savedRefreshToken);
|
||||||
|
|
||||||
|
// then
|
||||||
|
RefreshToken lastSavedRefreshToken = refreshRedisRepository.findById(id).get();
|
||||||
|
assertThat(lastSavedRefreshToken.getToken()).isEqualTo("refreshToken2");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.ticketing.server.movie.domain;
|
||||||
|
|
||||||
|
public class MovieTest {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package com.ticketing.server.movie.domain.repository;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.domain.Movie;
|
||||||
|
import java.util.Optional;
|
||||||
|
import javax.transaction.Transactional;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.MethodOrderer;
|
||||||
|
import org.junit.jupiter.api.Order;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.TestMethodOrder;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.annotation.Rollback;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@Transactional
|
||||||
|
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||||
|
public class MovieRepositoryTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
MovieRepository movieRepository;
|
||||||
|
|
||||||
|
@Order(1)
|
||||||
|
@Test
|
||||||
|
@Rollback(value = false)
|
||||||
|
@DisplayName("Movie Repository Test - saving movie")
|
||||||
|
void shouldAbleToSaveMovie() {
|
||||||
|
// given
|
||||||
|
Movie movie = new Movie("범죄도시 2", 106L);
|
||||||
|
|
||||||
|
// when
|
||||||
|
Movie savedMovie = movieRepository.save(movie);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(movie.getTitle(), savedMovie.getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Order(2)
|
||||||
|
@Test
|
||||||
|
@DisplayName("Movie Repository Test - finding movie with title")
|
||||||
|
void ShouldAbleToFindMovieWithTitle() {
|
||||||
|
// given, when
|
||||||
|
Optional<Movie> optionalMovie = movieRepository.findByTitle("범죄도시 2");
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(optionalMovie.isPresent());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Order(3)
|
||||||
|
@Test
|
||||||
|
@DisplayName("Movie Repository Test - finding movie that doesn't exist")
|
||||||
|
void ShouldNotAbleToFindMovie() {
|
||||||
|
// given, when
|
||||||
|
Optional<Movie> optionalMovie = movieRepository.findByTitle("존재하지 않는 영화");
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertFalse(optionalMovie.isPresent());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.ticketing.server.movie.domain.repository;
|
||||||
|
|
||||||
|
public class MovieTimesRepositoryTest {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.ticketing.server.movie.domain.repository;
|
||||||
|
|
||||||
|
public class SeatRepositoryTest {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.ticketing.server.movie.domain.repository;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.domain.Theater;
|
||||||
|
import javax.transaction.Transactional;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@Transactional
|
||||||
|
public class TheaterRepositoryTest {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
TheaterRepository theaterRepository;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Theater Repository Test - saving theater")
|
||||||
|
void ShouldAbleToSaveTheater() {
|
||||||
|
// given
|
||||||
|
Theater theater = new Theater(1);
|
||||||
|
|
||||||
|
// when
|
||||||
|
Theater savedTheater = theaterRepository.save(theater);
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertEquals(theater.getTheaterNumber(), savedTheater.getTheaterNumber());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.ticketing.server.movie.domain.repository;
|
||||||
|
|
||||||
|
public class TicketRepositoryTest {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package com.ticketing.server.movie.service;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
public class TMDBServiceImplTest {
|
||||||
|
|
||||||
|
@Value("${tmdb.api-key}")
|
||||||
|
private String apiKey;
|
||||||
|
|
||||||
|
@Value("${tmdb.read-access-token}")
|
||||||
|
private String readAccessToken;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RestTemplate restTemplate;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("TMDB Service Test - Get [Now Playing] movies")
|
||||||
|
void shouldAbleToGetMovieList() throws Exception {
|
||||||
|
// given
|
||||||
|
assertNotNull(apiKey);
|
||||||
|
assertNotNull(readAccessToken);
|
||||||
|
|
||||||
|
ArrayList<Charset> acceptCharset = new ArrayList<>();
|
||||||
|
acceptCharset.add(StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setAcceptCharset(acceptCharset);
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.setBearerAuth(readAccessToken);
|
||||||
|
|
||||||
|
Map<String, String> params = new HashMap<>();
|
||||||
|
params.put("api_key", apiKey);
|
||||||
|
params.put("language", "ko");
|
||||||
|
|
||||||
|
HttpEntity<?> request = new HttpEntity<>(headers);
|
||||||
|
|
||||||
|
// when
|
||||||
|
ResponseEntity<?> response = restTemplate.exchange(
|
||||||
|
"https://api.themoviedb.org/3/movie/now_playing?" + mapToUrlParam(params),
|
||||||
|
HttpMethod.GET,
|
||||||
|
request,
|
||||||
|
String.class
|
||||||
|
);
|
||||||
|
|
||||||
|
// JSONParser parser = new JSONParser();
|
||||||
|
// Object obj = parser.parse(String.valueOf(response));
|
||||||
|
// Object results = ((JSONObject) obj).get("results");
|
||||||
|
//
|
||||||
|
// ArrayList<String> movieList = new ArrayList<>();
|
||||||
|
//
|
||||||
|
// ArrayList<JSONObject> jsonMovieList = new ArrayList<>();
|
||||||
|
|
||||||
|
// then
|
||||||
|
assertTrue(response.getStatusCode().is2xxSuccessful());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String mapToUrlParam(Map<String, String> params) {
|
||||||
|
StringBuffer paramData = new StringBuffer();
|
||||||
|
|
||||||
|
for (Map.Entry<String, String> param : params.entrySet()) {
|
||||||
|
if (paramData.length() != 0) {
|
||||||
|
paramData.append('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
paramData.append(param.getKey());
|
||||||
|
paramData.append('=');
|
||||||
|
paramData.append(param.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
return paramData.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package com.ticketing.server.user.application;
|
||||||
|
|
||||||
|
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.ticketing.server.global.redis.RefreshRedisRepository;
|
||||||
|
import com.ticketing.server.user.application.request.LoginRequest;
|
||||||
|
import com.ticketing.server.user.application.request.SignUpRequest;
|
||||||
|
import com.ticketing.server.user.service.interfaces.UserService;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
import org.springframework.test.web.servlet.ResultActions;
|
||||||
|
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.context.WebApplicationContext;
|
||||||
|
|
||||||
|
@SpringBootTest
|
||||||
|
@Transactional
|
||||||
|
class AuthControllerTest {
|
||||||
|
|
||||||
|
private static final String LOGIN_URL = "/api/auth/token";
|
||||||
|
private static final String REGISTER_URL = "/api/users";
|
||||||
|
private static final String USER_EMAIL = "ticketing@gmail.com";
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
WebApplicationContext context;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
UserService userService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
RefreshRedisRepository refreshRedisRepository;
|
||||||
|
|
||||||
|
MockMvc mvc;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("로그인 인증 성공")
|
||||||
|
void loginSuccess() throws Exception {
|
||||||
|
// given
|
||||||
|
LoginRequest request = new LoginRequest(USER_EMAIL, "qwe123");
|
||||||
|
|
||||||
|
// when
|
||||||
|
ResultActions actions = mvc.perform(post(LOGIN_URL)
|
||||||
|
.content(asJsonString(request))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
|
// then
|
||||||
|
actions.andDo(print())
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("로그인 패스워드 인증 실패")
|
||||||
|
void loginPasswordFail() throws Exception {
|
||||||
|
// given
|
||||||
|
LoginRequest request = new LoginRequest(USER_EMAIL, "qwe1234");
|
||||||
|
|
||||||
|
// when
|
||||||
|
ResultActions actions = mvc.perform(post(LOGIN_URL)
|
||||||
|
.content(asJsonString(request))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON));
|
||||||
|
|
||||||
|
// then
|
||||||
|
actions.andDo(print())
|
||||||
|
.andExpect(status().isUnauthorized());
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void init() throws Exception {
|
||||||
|
mvc = MockMvcBuilders
|
||||||
|
.webAppContextSetup(context)
|
||||||
|
.apply(springSecurity())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
SignUpRequest signUpRequest = new SignUpRequest("ticketing", USER_EMAIL, "qwe123", "010-1234-5678");
|
||||||
|
|
||||||
|
mvc.perform(post(REGISTER_URL)
|
||||||
|
.content(asJsonString(signUpRequest))
|
||||||
|
.contentType(MediaType.APPLICATION_JSON));
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
refreshRedisRepository.deleteAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String asJsonString(Object object) throws JsonProcessingException {
|
||||||
|
return objectMapper.writeValueAsString(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package com.ticketing.server.user.application;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class UserControllerTest {
|
||||||
|
|
||||||
|
}
|
||||||
37
server/src/intTest/resources/application.yml
Normal file
37
server/src/intTest/resources/application.yml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
url: jdbc:mysql://localhost:3306/ticketing_test?serverTimezone=Asia/Seoul&characterEncoding=UTF-8
|
||||||
|
username: ENC(LowN1n4w0Ep/DqLD8+q5Bq6AXM4b8e3V)
|
||||||
|
password: ENC(OMvGcpZLpggFTiGNkqNe66Zq/SmJXF6o)
|
||||||
|
driver-class-name: com.mysql.cj.jdbc.Driver
|
||||||
|
redis:
|
||||||
|
host: localhost
|
||||||
|
port: 6379
|
||||||
|
|
||||||
|
jpa:
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
show_sql: true
|
||||||
|
format_sql: true
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: create
|
||||||
|
|
||||||
|
mvc:
|
||||||
|
pathmatch:
|
||||||
|
matching-strategy: ant_path_matcher
|
||||||
|
|
||||||
|
jasypt:
|
||||||
|
encryptor:
|
||||||
|
bean: jasyptStringEncryptor
|
||||||
|
|
||||||
|
jwt:
|
||||||
|
access-header: Authorization
|
||||||
|
refresh-header: REFRESH_TOKEN
|
||||||
|
prefix: Bearer
|
||||||
|
secret-key: Zi1sYWItdGlja2V0aW5nLXByb2plY3Qtc3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXNlY3JldC1rZXktZi1sYWItdGlja2V0aW5nLXByb2plY3Qtc3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXNlY3JldC1rZXkK
|
||||||
|
access-token-validity-in-seconds: 60
|
||||||
|
refresh-token-validity-in-seconds: 259200
|
||||||
|
|
||||||
|
tmdb:
|
||||||
|
api-key: 0d1503b6dcbfe1c514299b5564c649b8
|
||||||
|
read-access-token: eyJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIwZDE1MDNiNmRjYmZlMWM1MTQyOTliNTU2NGM2NDliOCIsInN1YiI6IjYyOWYwODRlNzI2ZmIxMTA2NDA4MjI2NCIsInNjb3BlcyI6WyJhcGlfcmVhZCJdLCJ2ZXJzaW9uIjoxfQ.rs8KZea8QLyashILiggWFx2s46lgUtzo-xSWoDgE58A
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ticketing.server;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.security.jwt.JwtProperties;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||||
|
|
||||||
|
@EnableJpaAuditing
|
||||||
|
@SpringBootApplication
|
||||||
|
@EnableConfigurationProperties(JwtProperties.class)
|
||||||
|
public class ServerApplication {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SpringApplication.run(ServerApplication.class, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.ticketing.server.global.config;
|
||||||
|
|
||||||
|
import org.jasypt.encryption.StringEncryptor;
|
||||||
|
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
|
||||||
|
import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class JasyptConfig {
|
||||||
|
|
||||||
|
@Bean("jasyptStringEncryptor")
|
||||||
|
public StringEncryptor stringEncryptor() {
|
||||||
|
PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
|
||||||
|
SimpleStringPBEConfig config = new SimpleStringPBEConfig();
|
||||||
|
config.setPassword("ticketing");
|
||||||
|
config.setAlgorithm("PBEWithMD5AndDES");
|
||||||
|
config.setKeyObtentionIterations("1000");
|
||||||
|
config.setPoolSize("1");
|
||||||
|
config.setProviderName("SunJCE");
|
||||||
|
config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
|
||||||
|
config.setStringOutputType("base64");
|
||||||
|
encryptor.setConfig(config);
|
||||||
|
return encryptor;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ticketing.server.global.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.support.ResourceBundleMessageSource;
|
||||||
|
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class MessagesConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public ResourceBundleMessageSource messageSource() {
|
||||||
|
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
|
||||||
|
messageSource.setBasename("i18n/messages");
|
||||||
|
messageSource.setDefaultEncoding("UTF-8");
|
||||||
|
|
||||||
|
return messageSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public LocalValidatorFactoryBean getValidator() {
|
||||||
|
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
|
||||||
|
factoryBean.setValidationMessageSource(messageSource());
|
||||||
|
|
||||||
|
return factoryBean;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ticketing.server.global.config;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.time.Duration;
|
||||||
|
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.client.BufferingClientHttpRequestFactory;
|
||||||
|
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||||
|
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class RestTemplateConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
|
||||||
|
return restTemplateBuilder
|
||||||
|
.requestFactory(() -> new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()))
|
||||||
|
.setConnectTimeout(Duration.ofMillis(5000)) // connection-timeout
|
||||||
|
.setReadTimeout(Duration.ofMillis(5000)) // read-timeout
|
||||||
|
.additionalMessageConverters(new StringHttpMessageConverter(Charset.forName("UTF-8")))
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.ticketing.server.global.config;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
|
||||||
|
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
|
||||||
|
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
|
||||||
|
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
|
||||||
|
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
|
||||||
|
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||||
|
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||||
|
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||||
|
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
|
||||||
|
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
|
||||||
|
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
|
||||||
|
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.core.env.Environment;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import springfox.documentation.builders.PathSelectors;
|
||||||
|
import springfox.documentation.builders.RequestHandlerSelectors;
|
||||||
|
import springfox.documentation.service.ApiKey;
|
||||||
|
import springfox.documentation.service.AuthorizationScope;
|
||||||
|
import springfox.documentation.service.SecurityReference;
|
||||||
|
import springfox.documentation.spi.DocumentationType;
|
||||||
|
import springfox.documentation.spi.service.contexts.SecurityContext;
|
||||||
|
import springfox.documentation.spring.web.plugins.Docket;
|
||||||
|
import springfox.documentation.swagger.web.UiConfiguration;
|
||||||
|
import springfox.documentation.swagger.web.UiConfigurationBuilder;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class SwaggerConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Docket api() {
|
||||||
|
return new Docket(DocumentationType.OAS_30).useDefaultResponseMessages(false).select()
|
||||||
|
.apis(RequestHandlerSelectors.any()).paths(PathSelectors.ant("/api/**")).build()
|
||||||
|
.securityContexts(Arrays.asList(securityContext()))
|
||||||
|
.securitySchemes(Arrays.asList(apiKey()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier, ServletEndpointsSupplier servletEndpointsSupplier, ControllerEndpointsSupplier controllerEndpointsSupplier, EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties, Environment environment) {
|
||||||
|
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
|
||||||
|
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
|
||||||
|
allEndpoints.addAll(webEndpoints);
|
||||||
|
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
|
||||||
|
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
|
||||||
|
String basePath = webEndpointProperties.getBasePath();
|
||||||
|
EndpointMapping endpointMapping = new EndpointMapping(basePath);
|
||||||
|
boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
|
||||||
|
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), shouldRegisterLinksMapping, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, String basePath) {
|
||||||
|
return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ApiKey apiKey() {
|
||||||
|
return new ApiKey(SECURITY_SCHEMA_NAME, "Authorization", "header");
|
||||||
|
}
|
||||||
|
|
||||||
|
private SecurityContext securityContext() {
|
||||||
|
return SecurityContext.builder().securityReferences(defaultAuth()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String SECURITY_SCHEMA_NAME = "Authorization";
|
||||||
|
public static final String AUTHORIZATION_SCOPE_GLOBAL = "global";
|
||||||
|
public static final String AUTHORIZATION_SCOPE_GLOBAL_DESC = "accessEverything";
|
||||||
|
|
||||||
|
private List<SecurityReference> defaultAuth() {
|
||||||
|
AuthorizationScope authorizationScope = new AuthorizationScope(AUTHORIZATION_SCOPE_GLOBAL,
|
||||||
|
AUTHORIZATION_SCOPE_GLOBAL_DESC);
|
||||||
|
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
|
||||||
|
authorizationScopes[0] = authorizationScope;
|
||||||
|
return Arrays.asList(new SecurityReference(SECURITY_SCHEMA_NAME, authorizationScopes));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
UiConfiguration uiConfig() {
|
||||||
|
return UiConfigurationBuilder.builder()
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.ticketing.server.global.dto.repository;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.EntityListeners;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.persistence.GenerationType;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import javax.persistence.MappedSuperclass;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.springframework.data.annotation.CreatedDate;
|
||||||
|
import org.springframework.data.annotation.LastModifiedDate;
|
||||||
|
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||||
|
|
||||||
|
@MappedSuperclass
|
||||||
|
@EntityListeners(AuditingEntityListener.class)
|
||||||
|
@Getter
|
||||||
|
public abstract class AbstractEntity {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
protected Long id;
|
||||||
|
|
||||||
|
@Column(nullable = false, updatable = false)
|
||||||
|
@CreatedDate
|
||||||
|
protected LocalDateTime createdAt;
|
||||||
|
|
||||||
|
@LastModifiedDate
|
||||||
|
protected LocalDateTime updatedAt;
|
||||||
|
|
||||||
|
protected LocalDateTime deletedAt;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package com.ticketing.server.global.exception;
|
||||||
|
|
||||||
|
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||||
|
import static org.springframework.http.HttpStatus.CONFLICT;
|
||||||
|
import static org.springframework.http.HttpStatus.FORBIDDEN;
|
||||||
|
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum ErrorCode {
|
||||||
|
|
||||||
|
/* 400 BAD_REQUEST : 잘못된 요청 */
|
||||||
|
MISMATCH_PASSWORD(BAD_REQUEST, "비밀번호가 일치하지 않습니다."),
|
||||||
|
TOKEN_TYPE(BAD_REQUEST, "토큰 타입이 올바르지 않습니다."),
|
||||||
|
UNAVAILABLE_REFRESH_TOKEN(BAD_REQUEST, "사용할 수 없는 토큰 입니다."),
|
||||||
|
UNABLE_CHANGE_GRADE(BAD_REQUEST, "동일한 등급으로 변경할 수 없습니다."),
|
||||||
|
|
||||||
|
/* 403 FORBIDDEN : 접근 권한 제한 */
|
||||||
|
VALID_USER_ID(FORBIDDEN, "해당 정보에 접근 권한이 존재하지 않습니다."),
|
||||||
|
|
||||||
|
/* 404 NOT_FOUND : Resource 를 찾을 수 없음 */
|
||||||
|
USER_NOT_FOUND(NOT_FOUND, "해당 유저 정보를 찾을 수 없습니다."),
|
||||||
|
EMAIL_NOT_FOUND(NOT_FOUND, "해당 이메일을 찾을 수 없습니다."),
|
||||||
|
MOVIE_NOT_FOUND(NOT_FOUND, "해당 제목의 영화를 찾을 수 없습니다."),
|
||||||
|
REFRESH_TOKEN_NOT_FOUND(NOT_FOUND, "리프레쉬 토큰을 찾을 수 없습니다."),
|
||||||
|
PAYMENT_ID_NOT_FOUND(NOT_FOUND, "결제정보를 찾을 수 없습니다."),
|
||||||
|
|
||||||
|
/* 409 CONFLICT : Resource 의 현재 상태와 충돌. 보통 중복된 데이터 존재 */
|
||||||
|
DUPLICATE_EMAIL(CONFLICT, "이메일이 이미 존재합니다."),
|
||||||
|
DUPLICATE_PAYMENT(CONFLICT, "해당 좌석은 현재 판매된 좌석입니다."),
|
||||||
|
DUPLICATE_MOVIE(CONFLICT, "해당 영화 정보가 이미 존재합니다."),
|
||||||
|
DELETED_EMAIL(CONFLICT, "이미 삭제된 이메일 입니다.");
|
||||||
|
|
||||||
|
private final HttpStatus httpStatus;
|
||||||
|
private final String detail;
|
||||||
|
|
||||||
|
/* 400 BAD_REQUEST : 잘못된 요청 */
|
||||||
|
public static TicketingException throwMismatchPassword() {
|
||||||
|
throw new TicketingException(MISMATCH_PASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TicketingException throwTokenType() {
|
||||||
|
throw new TicketingException(TOKEN_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TicketingException throwUnavailableRefreshToken() {
|
||||||
|
throw new TicketingException(UNAVAILABLE_REFRESH_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TicketingException throwUnableChangeGrade() {
|
||||||
|
throw new TicketingException(UNABLE_CHANGE_GRADE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 403 FORBIDDEN : 접근 권한 제한 */
|
||||||
|
public static TicketingException throwValidUserId() {
|
||||||
|
throw new TicketingException(VALID_USER_ID);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 404 NOT_FOUND : Resource 를 찾을 수 없음 */
|
||||||
|
public static TicketingException throwUserNotFound() {
|
||||||
|
throw new TicketingException(USER_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TicketingException throwEmailNotFound() {
|
||||||
|
throw new TicketingException(EMAIL_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TicketingException throwMovieNotFound() {
|
||||||
|
throw new TicketingException(MOVIE_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TicketingException throwRefreshTokenNotFound() {
|
||||||
|
throw new TicketingException(REFRESH_TOKEN_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TicketingException throwPaymentIdNotFound() {
|
||||||
|
throw new TicketingException(PAYMENT_ID_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 409 CONFLICT : Resource 의 현재 상태와 충돌. 보통 중복된 데이터 존재 */
|
||||||
|
public static TicketingException throwDuplicateEmail() {
|
||||||
|
throw new TicketingException(DUPLICATE_EMAIL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TicketingException throwDuplicatePayment() {
|
||||||
|
throw new TicketingException(DUPLICATE_PAYMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TicketingException throwDuplicateMovie() {
|
||||||
|
throw new TicketingException(DUPLICATE_MOVIE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TicketingException throwDeletedEmail() {
|
||||||
|
throw new TicketingException(DELETED_EMAIL);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ticketing.server.global.exception;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.Getter;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@EqualsAndHashCode
|
||||||
|
public class ErrorResponse {
|
||||||
|
|
||||||
|
private final HttpStatus status;
|
||||||
|
private final String message;
|
||||||
|
private final List<String> errors;
|
||||||
|
|
||||||
|
public ErrorResponse(HttpStatus status, String message, List<String> errors) {
|
||||||
|
this.status = status;
|
||||||
|
this.message = message;
|
||||||
|
this.errors = errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErrorResponse(HttpStatus status, String message, String error) {
|
||||||
|
this.status = status;
|
||||||
|
this.message = message;
|
||||||
|
this.errors = List.of(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ErrorResponse toErrorResponse(ErrorCode errorCode) {
|
||||||
|
return new ErrorResponse(
|
||||||
|
errorCode.getHttpStatus(),
|
||||||
|
errorCode.name(),
|
||||||
|
errorCode.getDetail());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
package com.ticketing.server.global.exception;
|
||||||
|
|
||||||
|
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||||
|
import static org.springframework.http.HttpStatus.FORBIDDEN;
|
||||||
|
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
|
||||||
|
import static org.springframework.http.HttpStatus.METHOD_NOT_ALLOWED;
|
||||||
|
import static org.springframework.http.HttpStatus.NOT_FOUND;
|
||||||
|
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
|
||||||
|
import static org.springframework.http.HttpStatus.UNSUPPORTED_MEDIA_TYPE;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import javax.validation.ConstraintViolation;
|
||||||
|
import javax.validation.ConstraintViolationException;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.TypeMismatchException;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.validation.BindException;
|
||||||
|
import org.springframework.validation.FieldError;
|
||||||
|
import org.springframework.validation.ObjectError;
|
||||||
|
import org.springframework.web.HttpMediaTypeNotSupportedException;
|
||||||
|
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.MissingServletRequestParameterException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
import org.springframework.web.context.request.WebRequest;
|
||||||
|
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||||
|
import org.springframework.web.multipart.support.MissingServletRequestPartException;
|
||||||
|
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
|
||||||
|
|
||||||
|
/* 400 START */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valid 유효성 검사 실패
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected ResponseEntity<Object> handleMethodArgumentNotValid(
|
||||||
|
MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
|
||||||
|
log.error("MethodArgumentNotValidException :: ", ex);
|
||||||
|
|
||||||
|
List<String> errors = generateErrors(ex);
|
||||||
|
ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), errors);
|
||||||
|
|
||||||
|
return ResponseEntity.status(response.getStatus()).headers(headers).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/***
|
||||||
|
* ModelAttribute 으로 binding error 발생
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected ResponseEntity<Object> handleBindException(
|
||||||
|
BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
|
||||||
|
log.error("BindException :: ", ex);
|
||||||
|
|
||||||
|
List<String> errors = generateErrors(ex);
|
||||||
|
ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), errors);
|
||||||
|
return ResponseEntity.status(response.getStatus()).headers(headers).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 잘못된 유형으로 Bean 속성 설정
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected ResponseEntity<Object> handleTypeMismatch(
|
||||||
|
TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
|
||||||
|
log.error("TypeMismatchException :: ", ex);
|
||||||
|
|
||||||
|
String error = ex.getValue() + " value for " + ex.getPropertyName() + " should be of type " + ex.getRequiredType();
|
||||||
|
ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), error);
|
||||||
|
return ResponseEntity.status(response.getStatus()).headers(headers).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* multipart/form-data 요청 실패
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected ResponseEntity<Object> handleMissingServletRequestPart(
|
||||||
|
MissingServletRequestPartException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
|
||||||
|
log.error("MissingServletRequestPartException :: ", ex);
|
||||||
|
|
||||||
|
String error = ex.getRequestPartName() + " part is missing";
|
||||||
|
ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), error);
|
||||||
|
return ResponseEntity.status(response.getStatus()).headers(headers).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 필수 인수 누락
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected ResponseEntity<Object> handleMissingServletRequestParameter(
|
||||||
|
MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
|
||||||
|
log.error("MissingServletRequestParameterException :: ", ex);
|
||||||
|
|
||||||
|
String error = ex.getParameterName() + " parameter is missing";
|
||||||
|
ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), error);
|
||||||
|
return ResponseEntity.status(response.getStatus()).headers(headers).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인수가 예상한 형식이 아닐 시
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(value = MethodArgumentTypeMismatchException.class)
|
||||||
|
protected ResponseEntity<Object> handleMethodArgumentTypeMismatch(MethodArgumentTypeMismatchException ex) {
|
||||||
|
log.error("MethodArgumentTypeMismatchException :: ", ex);
|
||||||
|
|
||||||
|
String error = ex.getName() + " should be of type " + Objects.requireNonNull(ex.getRequiredType()).getName();
|
||||||
|
ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), error);
|
||||||
|
return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 제약 조건 위반
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(value = ConstraintViolationException.class)
|
||||||
|
protected ResponseEntity<Object> handleConstraintViolation(ConstraintViolationException ex) {
|
||||||
|
log.error("ConstraintViolationException :: ", ex);
|
||||||
|
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
for (ConstraintViolation<?> violation : ex.getConstraintViolations()) {
|
||||||
|
errors.add(violation.getRootBeanClass().getName() + " " + violation.getPropertyPath() + ": " + violation.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorResponse response = new ErrorResponse(BAD_REQUEST, ex.getLocalizedMessage(), errors);
|
||||||
|
return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 400 END */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 404 발생
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected ResponseEntity<Object> handleNoHandlerFoundException(
|
||||||
|
NoHandlerFoundException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
|
||||||
|
log.error("NoHandlerFoundException :: ", ex);
|
||||||
|
|
||||||
|
String error = "No handler found for " + ex.getHttpMethod() + " " + ex.getRequestURL();
|
||||||
|
ErrorResponse response = new ErrorResponse(NOT_FOUND, ex.getLocalizedMessage(), error);
|
||||||
|
return ResponseEntity.status(response.getStatus()).headers(headers).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지원하지 않는 HTTP 메서드로 요청 405
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
|
||||||
|
HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
|
||||||
|
log.error("HttpRequestMethodNotSupportedException :: ", ex);
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append(ex.getMethod());
|
||||||
|
builder.append(" method is not supported for this request. Supported methods are ");
|
||||||
|
|
||||||
|
Set<HttpMethod> supportedHttpMethods = ex.getSupportedHttpMethods();
|
||||||
|
if (supportedHttpMethods != null) {
|
||||||
|
supportedHttpMethods.forEach(t -> builder.append(t).append(" "));
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorResponse response = new ErrorResponse(METHOD_NOT_ALLOWED, ex.getLocalizedMessage(), builder.toString());
|
||||||
|
return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 지원되지 않는 미디어 유형으로 요청 415
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected ResponseEntity<Object> handleHttpMediaTypeNotSupported(
|
||||||
|
HttpMediaTypeNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
|
||||||
|
log.error("HttpMediaTypeNotSupportedException :: ", ex);
|
||||||
|
|
||||||
|
StringBuilder builder = new StringBuilder();
|
||||||
|
builder.append(ex.getContentType());
|
||||||
|
builder.append(" media type is not supported. Supported media types are ");
|
||||||
|
ex.getSupportedMediaTypes().forEach(t -> builder.append(t).append(" "));
|
||||||
|
|
||||||
|
ErrorResponse response = new ErrorResponse(UNSUPPORTED_MEDIA_TYPE, ex.getLocalizedMessage(), builder.substring(0, builder.length() - 2));
|
||||||
|
return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 특정 핸들러 없는 모든 예외 500
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(value = Exception.class)
|
||||||
|
protected ResponseEntity<Object> handleAll(Exception ex) {
|
||||||
|
log.error("Exception :: ", ex);
|
||||||
|
|
||||||
|
ErrorResponse response = new ErrorResponse(INTERNAL_SERVER_ERROR, ex.getLocalizedMessage(), "오류가 발생했습니다.");
|
||||||
|
return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 접근 권한이 없을 때
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(value = AccessDeniedException.class)
|
||||||
|
protected ResponseEntity<ErrorResponse> handleAccessDeniedException(Exception ex) {
|
||||||
|
log.error("AccessDeniedException :: ", ex);
|
||||||
|
|
||||||
|
ErrorResponse response = new ErrorResponse(FORBIDDEN, ex.getLocalizedMessage(), "접근 권한이 없습니다.");
|
||||||
|
return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 인증 정보가 없을 때
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(value = AuthenticationException.class)
|
||||||
|
protected ResponseEntity<ErrorResponse> handleAuthenticationException(Exception ex) {
|
||||||
|
log.error("AuthenticationException :: ", ex);
|
||||||
|
|
||||||
|
ErrorResponse response = new ErrorResponse(UNAUTHORIZED, ex.getLocalizedMessage(), "로그인 후 이용하실 수 있습니다.");
|
||||||
|
return ResponseEntity.status(response.getStatus()).headers(new HttpHeaders()).body(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 커스텀 예외 발생 시
|
||||||
|
*/
|
||||||
|
@ExceptionHandler(value = TicketingException.class)
|
||||||
|
protected ResponseEntity<ErrorResponse> ticketingException(TicketingException ex) {
|
||||||
|
log.error("TicketingException :: ", ex);
|
||||||
|
|
||||||
|
ErrorCode errorCode = ex.getErrorCode();
|
||||||
|
return ResponseEntity.status(errorCode.getHttpStatus()).body(ErrorResponse.toErrorResponse(errorCode));
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<String> generateErrors(BindException ex) {
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
List<ObjectError> allErrors = ex.getBindingResult().getAllErrors();
|
||||||
|
|
||||||
|
for (ObjectError error : allErrors) {
|
||||||
|
errors.add(error.getDefaultMessage());
|
||||||
|
}
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package com.ticketing.server.global.exception;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TicketingException extends RuntimeException {
|
||||||
|
|
||||||
|
private final ErrorCode errorCode;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ticketing.server.global.factory;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Properties;
|
||||||
|
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
|
||||||
|
import org.springframework.core.env.PropertiesPropertySource;
|
||||||
|
import org.springframework.core.env.PropertySource;
|
||||||
|
import org.springframework.core.io.support.EncodedResource;
|
||||||
|
import org.springframework.core.io.support.PropertySourceFactory;
|
||||||
|
import org.springframework.lang.Nullable;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
public class YamlPropertySourceFactory implements PropertySourceFactory {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PropertySource<?> createPropertySource(@Nullable String name, EncodedResource resource) {
|
||||||
|
Properties yamlProperties = loadYamlProperties(resource);
|
||||||
|
String sourceName = StringUtils.hasText(name) ? name : resource.getResource().getFilename();
|
||||||
|
return new PropertiesPropertySource(Objects.requireNonNull(sourceName), Objects.requireNonNull(yamlProperties));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Properties loadYamlProperties(EncodedResource resource) {
|
||||||
|
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||||
|
factory.setResources(resource.getResource());
|
||||||
|
return factory.getObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.ticketing.server.global.health;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.boot.actuate.health.Health;
|
||||||
|
import org.springframework.boot.actuate.health.Status;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/l7check")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class L7checkController {
|
||||||
|
|
||||||
|
private final MutableHealthIndicator indicator;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<Object> health() {
|
||||||
|
Health health = indicator.health();
|
||||||
|
boolean isUp = health.getStatus().equals(Status.UP);
|
||||||
|
return ResponseEntity
|
||||||
|
.status(isUp ? HttpStatus.OK : HttpStatus.SERVICE_UNAVAILABLE)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping
|
||||||
|
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||||
|
public void down() {
|
||||||
|
indicator.setHealth(Health.down().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
@ResponseStatus(HttpStatus.CREATED)
|
||||||
|
public void up() {
|
||||||
|
indicator.setHealth(Health.up().build());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.ticketing.server.global.health;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
import org.springframework.boot.actuate.health.Health;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class ManualHealthIndicator implements MutableHealthIndicator {
|
||||||
|
|
||||||
|
private final AtomicReference<Health> healthRef = new AtomicReference<>(Health.up().build());
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setHealth(Health health) {
|
||||||
|
healthRef.set(health);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Health health() {
|
||||||
|
return healthRef.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.ticketing.server.global.health;
|
||||||
|
|
||||||
|
import org.springframework.boot.actuate.health.Health;
|
||||||
|
import org.springframework.boot.actuate.health.HealthIndicator;
|
||||||
|
|
||||||
|
public interface MutableHealthIndicator extends HealthIndicator {
|
||||||
|
|
||||||
|
void setHealth(Health health);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ticketing.server.global.redis;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
|
||||||
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
import org.springframework.orm.jpa.JpaTransactionManager;
|
||||||
|
import org.springframework.transaction.PlatformTransactionManager;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableRedisRepositories
|
||||||
|
public class RedisConfig {
|
||||||
|
|
||||||
|
@Value("${spring.redis.host}")
|
||||||
|
private String host;
|
||||||
|
|
||||||
|
@Value("${spring.redis.port}")
|
||||||
|
private int port;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RedisConnectionFactory redisConnectionFactory() {
|
||||||
|
return new LettuceConnectionFactory(host, port);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RedisTemplate<String, Object> redisTemplate() {
|
||||||
|
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
|
||||||
|
redisTemplate.setConnectionFactory(redisConnectionFactory());
|
||||||
|
redisTemplate.setKeySerializer(new StringRedisSerializer());
|
||||||
|
redisTemplate.setValueSerializer(new StringRedisSerializer());
|
||||||
|
|
||||||
|
return redisTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PlatformTransactionManager transactionManager() {
|
||||||
|
return new JpaTransactionManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.ticketing.server.global.redis;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.repository.CrudRepository;
|
||||||
|
|
||||||
|
public interface RefreshRedisRepository extends CrudRepository<RefreshToken, Long> {
|
||||||
|
|
||||||
|
Optional<RefreshToken> findByEmail(String email);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package com.ticketing.server.global.redis;
|
||||||
|
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.GeneratedValue;
|
||||||
|
import javax.persistence.Id;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import org.springframework.data.redis.core.RedisHash;
|
||||||
|
import org.springframework.data.redis.core.index.Indexed;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RedisHash("RefreshToken")
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
public class RefreshToken {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue
|
||||||
|
@Column(name = "refresh_token_id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Indexed
|
||||||
|
private String email;
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
public RefreshToken(String email, String token) {
|
||||||
|
this.email = email;
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeToken(String token) {
|
||||||
|
this.token = token;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.ticketing.server.global.security;
|
||||||
|
|
||||||
|
import com.ticketing.server.user.domain.UserGrade;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.access.AccessDecisionManager;
|
||||||
|
import org.springframework.security.access.AccessDecisionVoter;
|
||||||
|
import org.springframework.security.access.annotation.Jsr250Voter;
|
||||||
|
import org.springframework.security.access.expression.method.ExpressionBasedPreInvocationAdvice;
|
||||||
|
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||||
|
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
|
||||||
|
import org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter;
|
||||||
|
import org.springframework.security.access.vote.AffirmativeBased;
|
||||||
|
import org.springframework.security.access.vote.AuthenticatedVoter;
|
||||||
|
import org.springframework.security.access.vote.RoleHierarchyVoter;
|
||||||
|
import org.springframework.security.access.vote.RoleVoter;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
|
||||||
|
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
|
||||||
|
|
||||||
|
@EnableGlobalMethodSecurity(
|
||||||
|
securedEnabled = true,
|
||||||
|
jsr250Enabled = true,
|
||||||
|
prePostEnabled = true
|
||||||
|
)
|
||||||
|
@Configuration
|
||||||
|
public class RoleConfig extends GlobalMethodSecurityConfiguration {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected AccessDecisionManager accessDecisionManager() {
|
||||||
|
List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
|
||||||
|
ExpressionBasedPreInvocationAdvice expressionAdvice = new ExpressionBasedPreInvocationAdvice();
|
||||||
|
expressionAdvice.setExpressionHandler(getExpressionHandler());
|
||||||
|
decisionVoters.add(new PreInvocationAuthorizationAdviceVoter(expressionAdvice));
|
||||||
|
decisionVoters.add(new Jsr250Voter());
|
||||||
|
|
||||||
|
decisionVoters.add(new RoleVoter());
|
||||||
|
decisionVoters.add(roleHierarchyVoter());
|
||||||
|
decisionVoters.add(new AuthenticatedVoter());
|
||||||
|
return new AffirmativeBased(decisionVoters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RoleHierarchyVoter roleHierarchyVoter() {
|
||||||
|
return new RoleHierarchyVoter(roleHierarchy());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RoleHierarchy roleHierarchy() {
|
||||||
|
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
|
||||||
|
roleHierarchy.setHierarchy(UserGrade.getRoleHierarchy());
|
||||||
|
return roleHierarchy;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ticketing.server.global.security;
|
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
|
public class SecurityUtil {
|
||||||
|
|
||||||
|
private SecurityUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getCurrentUserEmail() {
|
||||||
|
final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
|
if (authentication == null || authentication.getName() == null) {
|
||||||
|
throw new IllegalStateException("Security Context 에 인증 정보가 없습니다.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return authentication.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.ticketing.server.global.security;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.security.jwt.JwtFilter;
|
||||||
|
import com.ticketing.server.global.security.jwt.JwtSecurityConfig;
|
||||||
|
import com.ticketing.server.global.security.jwt.handle.JwtAccessDeniedHandler;
|
||||||
|
import com.ticketing.server.global.security.jwt.handle.JwtAuthenticationEntryPoint;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.http.HttpMethod;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||||
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||||
|
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
|
||||||
|
|
||||||
|
private final JwtFilter jwtFilter;
|
||||||
|
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
|
||||||
|
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void configure(HttpSecurity http) throws Exception {
|
||||||
|
|
||||||
|
http
|
||||||
|
.csrf().disable()
|
||||||
|
.exceptionHandling()
|
||||||
|
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
|
||||||
|
.accessDeniedHandler(jwtAccessDeniedHandler)
|
||||||
|
|
||||||
|
.and()
|
||||||
|
.headers()
|
||||||
|
.frameOptions()
|
||||||
|
.sameOrigin()
|
||||||
|
|
||||||
|
// 시큐리티는 기본적으로 세션을 사용하지만, jwt 을 위해 세션을 Stateless 로 설정
|
||||||
|
.and()
|
||||||
|
.sessionManagement()
|
||||||
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||||
|
|
||||||
|
.and()
|
||||||
|
.authorizeRequests()
|
||||||
|
.antMatchers(HttpMethod.POST, "/api/auth/token").permitAll()
|
||||||
|
.antMatchers(HttpMethod.POST, "/api/auth/refresh").permitAll()
|
||||||
|
.antMatchers(HttpMethod.POST, "/api/users").permitAll()
|
||||||
|
.antMatchers(HttpMethod.GET,"/api/movies").permitAll()
|
||||||
|
.antMatchers("/api/movieTimes/**").permitAll()
|
||||||
|
.antMatchers("/l7check").permitAll()
|
||||||
|
.antMatchers("/actuator/**").permitAll()
|
||||||
|
.antMatchers("/api/v3/", "/swagger-ui/**", "/swagger/", "/swagger-resources/**", "/v3/api-docs").permitAll()
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
.and()
|
||||||
|
.apply(new JwtSecurityConfig(jwtFilter));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.ticketing.server.global.security.jwt;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import javax.servlet.FilterChain;
|
||||||
|
import javax.servlet.ServletException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@Configuration
|
||||||
|
public class JwtFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
|
private final JwtProvider tokenProvider;
|
||||||
|
private final JwtProperties jwtProperties;
|
||||||
|
|
||||||
|
public JwtFilter(JwtProperties jwtProperties, JwtProvider tokenProvider) {
|
||||||
|
this.jwtProperties = jwtProperties;
|
||||||
|
this.tokenProvider = tokenProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||||
|
String jwt = resolveToken(request);
|
||||||
|
|
||||||
|
// 토큰이 정상이면 Authentication 을 가져와서 SecurityContext 에 저장
|
||||||
|
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
|
||||||
|
Authentication authentication = tokenProvider.getAuthentication(jwt);
|
||||||
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
|
}
|
||||||
|
|
||||||
|
chain.doFilter(request, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveToken(HttpServletRequest request) {
|
||||||
|
String bearerToken = request.getHeader(jwtProperties.getAccessHeader());
|
||||||
|
if (StringUtils.hasText(bearerToken) && jwtProperties.hasTokenStartsWith(bearerToken)) {
|
||||||
|
return bearerToken.substring(7);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.ticketing.server.global.security.jwt;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.factory.YamlPropertySourceFactory;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.boot.context.properties.ConstructorBinding;
|
||||||
|
import org.springframework.context.annotation.PropertySource;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@ConstructorBinding
|
||||||
|
@ConfigurationProperties(value = "jwt")
|
||||||
|
@PropertySource(value = "classpath:application.yml", factory = YamlPropertySourceFactory.class)
|
||||||
|
public class JwtProperties {
|
||||||
|
|
||||||
|
private final String accessHeader;
|
||||||
|
private final String refreshHeader;
|
||||||
|
private final String prefix;
|
||||||
|
private final String secretKey;
|
||||||
|
private final Integer accessTokenValidityInSeconds;
|
||||||
|
private final Integer refreshTokenValidityInSeconds;
|
||||||
|
|
||||||
|
public boolean hasTokenStartsWith(String token) {
|
||||||
|
return token.startsWith(prefix);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package com.ticketing.server.global.security.jwt;
|
||||||
|
|
||||||
|
import com.ticketing.server.user.service.dto.TokenDTO;
|
||||||
|
import io.jsonwebtoken.Claims;
|
||||||
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import io.jsonwebtoken.SignatureAlgorithm;
|
||||||
|
import io.jsonwebtoken.io.Decoders;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
|
import java.security.Key;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.StringJoiner;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.User;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class JwtProvider {
|
||||||
|
|
||||||
|
private static final String AUTHORITIES_KEY = "auth";
|
||||||
|
private static final String AUTHORITIES_DELIMITER = ",";
|
||||||
|
|
||||||
|
private final Key key;
|
||||||
|
private final String prefix;
|
||||||
|
private final long accessTokenValidityInMilliseconds;
|
||||||
|
private final long refreshTokenValidityInMilliseconds;
|
||||||
|
|
||||||
|
public JwtProvider(JwtProperties jwtProperties) {
|
||||||
|
byte[] keyBytes = Decoders.BASE64.decode(jwtProperties.getSecretKey());
|
||||||
|
this.key = Keys.hmacShaKeyFor(keyBytes);
|
||||||
|
|
||||||
|
this.prefix = jwtProperties.getPrefix();
|
||||||
|
this.accessTokenValidityInMilliseconds = jwtProperties.getAccessTokenValidityInSeconds() * 1000L;
|
||||||
|
this.refreshTokenValidityInMilliseconds = jwtProperties.getRefreshTokenValidityInSeconds() * 1000L;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TokenDTO generateTokenDto(Authentication authentication) {
|
||||||
|
String accessToken = createAccessToken(authentication);
|
||||||
|
String refreshToken = createRefreshToken(authentication);
|
||||||
|
long expiresIn = accessTokenValidityInMilliseconds / 1000L;
|
||||||
|
|
||||||
|
return new TokenDTO(accessToken, refreshToken, prefix, expiresIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createAccessToken(Authentication authentication) {
|
||||||
|
// 만료시간 계산
|
||||||
|
long now = (new Date()).getTime();
|
||||||
|
Date accessTokenExpiresIn = new Date(now + this.accessTokenValidityInMilliseconds);
|
||||||
|
|
||||||
|
return createToken(authentication, accessTokenExpiresIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createRefreshToken(Authentication authentication) {
|
||||||
|
// 만료시간 계산
|
||||||
|
long now = (new Date()).getTime();
|
||||||
|
Date refreshTokenExpiresIn = new Date(now + this.refreshTokenValidityInMilliseconds);
|
||||||
|
|
||||||
|
return createToken(authentication, refreshTokenExpiresIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String createToken(Authentication authentication, Date expiration) {
|
||||||
|
// 권한 정보 가져오기
|
||||||
|
String authorities = generateStringToAuthorities(authentication);
|
||||||
|
|
||||||
|
// JWT 생성
|
||||||
|
return Jwts.builder()
|
||||||
|
.setSubject(authentication.getName()) // email
|
||||||
|
.claim(AUTHORITIES_KEY, authorities) // payload
|
||||||
|
.setExpiration(expiration) // 만료일
|
||||||
|
.signWith(key, SignatureAlgorithm.HS512) // 서명 키 값
|
||||||
|
.compact();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String generateStringToAuthorities(Authentication authentication) {
|
||||||
|
StringJoiner authorities = new StringJoiner(AUTHORITIES_DELIMITER);
|
||||||
|
for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
|
||||||
|
String roleName = makeRoleName(grantedAuthority.getAuthority());
|
||||||
|
authorities.add(roleName);
|
||||||
|
}
|
||||||
|
return authorities.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String makeRoleName(String role) {
|
||||||
|
return "ROLE_" + role.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Authentication getAuthentication(String token) {
|
||||||
|
// 토큰 복호화
|
||||||
|
Claims claims = parseClaims(token);
|
||||||
|
|
||||||
|
// 권한조회
|
||||||
|
List<SimpleGrantedAuthority> authorities =
|
||||||
|
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(AUTHORITIES_DELIMITER))
|
||||||
|
.map(SimpleGrantedAuthority::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
UserDetails principal = new User(claims.getSubject(), "", authorities);
|
||||||
|
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean validateToken(String token) {
|
||||||
|
parseClaims(token);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Claims parseClaims(String token) {
|
||||||
|
return Jwts.parserBuilder()
|
||||||
|
.setSigningKey(key)
|
||||||
|
.build()
|
||||||
|
.parseClaimsJws(token)
|
||||||
|
.getBody();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ticketing.server.global.security.jwt;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.web.DefaultSecurityFilterChain;
|
||||||
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class JwtSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
|
||||||
|
|
||||||
|
private final JwtFilter jwtFilter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configure(HttpSecurity builder) {
|
||||||
|
builder.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.ticketing.server.global.security.jwt.handle;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.HandlerExceptionResolver;
|
||||||
|
|
||||||
|
@Component("JwtAccessDeniedHandler")
|
||||||
|
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
|
||||||
|
|
||||||
|
private final HandlerExceptionResolver resolver;
|
||||||
|
|
||||||
|
public JwtAccessDeniedHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
|
||||||
|
this.resolver = resolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
|
||||||
|
response.sendError(HttpServletResponse.SC_FORBIDDEN);
|
||||||
|
resolver.resolveException(request, response, null, accessDeniedException);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.ticketing.server.global.security.jwt.handle;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.security.core.AuthenticationException;
|
||||||
|
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.servlet.HandlerExceptionResolver;
|
||||||
|
|
||||||
|
@Component("JwtAuthenticationEntryPoint")
|
||||||
|
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||||
|
|
||||||
|
private final HandlerExceptionResolver resolver;
|
||||||
|
|
||||||
|
public JwtAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
|
||||||
|
this.resolver = resolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
|
||||||
|
resolver.resolveException(request, response, null, authException);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ticketing.server.global.validator.constraints;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.validator.constraintvalidators.FieldsValueNotMatchValidator;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
import javax.validation.Constraint;
|
||||||
|
import javax.validation.Payload;
|
||||||
|
|
||||||
|
@Constraint(validatedBy = FieldsValueNotMatchValidator.class)
|
||||||
|
@Target(ElementType.TYPE)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface FieldsValueNotMatch {
|
||||||
|
|
||||||
|
String message() default "validation.password.not.change";
|
||||||
|
|
||||||
|
String field();
|
||||||
|
|
||||||
|
String fieldMatch();
|
||||||
|
|
||||||
|
Class<?>[] groups() default {};
|
||||||
|
|
||||||
|
Class<? extends Payload>[] payload() default {};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ticketing.server.global.validator.constraints;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.validator.constraintvalidators.PhoneValidator;
|
||||||
|
import java.lang.annotation.Documented;
|
||||||
|
import java.lang.annotation.ElementType;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
import java.lang.annotation.Target;
|
||||||
|
import javax.validation.Constraint;
|
||||||
|
import javax.validation.Payload;
|
||||||
|
|
||||||
|
@Target(ElementType.FIELD)
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Constraint(validatedBy = {PhoneValidator.class})
|
||||||
|
@Documented
|
||||||
|
public @interface Phone {
|
||||||
|
|
||||||
|
String message() default "{validation.phone}";
|
||||||
|
|
||||||
|
Class<?>[] groups() default {};
|
||||||
|
|
||||||
|
Class<? extends Payload>[] payload() default {};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.ticketing.server.global.validator.constraintvalidators;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.validator.constraints.FieldsValueNotMatch;
|
||||||
|
import javax.validation.ConstraintValidator;
|
||||||
|
import javax.validation.ConstraintValidatorContext;
|
||||||
|
import org.springframework.beans.BeanWrapperImpl;
|
||||||
|
|
||||||
|
public class FieldsValueNotMatchValidator implements ConstraintValidator<FieldsValueNotMatch, Object> {
|
||||||
|
|
||||||
|
private String field;
|
||||||
|
private String fieldMatch;
|
||||||
|
|
||||||
|
public void initialize(FieldsValueNotMatch constraintAnnotation) {
|
||||||
|
this.field = constraintAnnotation.field();
|
||||||
|
this.fieldMatch = constraintAnnotation.fieldMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(Object value, ConstraintValidatorContext context) {
|
||||||
|
Object fieldValue = new BeanWrapperImpl(value).getPropertyValue(field);
|
||||||
|
Object fieldMatchValue = new BeanWrapperImpl(value).getPropertyValue(fieldMatch);
|
||||||
|
|
||||||
|
if (fieldValue != null) {
|
||||||
|
return !fieldValue.equals(fieldMatchValue);
|
||||||
|
} else {
|
||||||
|
return fieldMatchValue != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ticketing.server.global.validator.constraintvalidators;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.validator.constraints.Phone;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import javax.validation.ConstraintValidator;
|
||||||
|
import javax.validation.ConstraintValidatorContext;
|
||||||
|
|
||||||
|
public class PhoneValidator implements ConstraintValidator<Phone, String> {
|
||||||
|
|
||||||
|
private static final String PATTERN = "\\d{3}-\\d{4}-\\d{4}";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Pattern.matches(PATTERN, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.ticketing.server.movie.application;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.application.request.MovieRegisterRequest;
|
||||||
|
import com.ticketing.server.movie.application.response.MovieListResponse;
|
||||||
|
import com.ticketing.server.movie.application.response.MovieTitleResponse;
|
||||||
|
import com.ticketing.server.movie.service.interfaces.MovieService;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import javax.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.annotation.Secured;
|
||||||
|
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;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/movies")
|
||||||
|
@Api(value = "Movie API", tags = {"Movie"})
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class MovieController {
|
||||||
|
|
||||||
|
private final MovieService movieService;
|
||||||
|
|
||||||
|
@PostMapping()
|
||||||
|
@ApiOperation(value = "영화 정보 등록")
|
||||||
|
@Secured("ROLE_STAFF")
|
||||||
|
public ResponseEntity<MovieTitleResponse> registerMovie(@RequestBody @Valid MovieRegisterRequest request) {
|
||||||
|
return ResponseEntity.status(HttpStatus.OK)
|
||||||
|
.body(
|
||||||
|
MovieTitleResponse.from(
|
||||||
|
movieService.registerMovie(request.toMovieRegisterDTO())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping()
|
||||||
|
@ApiOperation(value = "영화 목록 조회")
|
||||||
|
public ResponseEntity<MovieListResponse> getMovies() {
|
||||||
|
return ResponseEntity.status(HttpStatus.OK)
|
||||||
|
.body(
|
||||||
|
MovieListResponse.from(
|
||||||
|
movieService.getMovies()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.ticketing.server.movie.application;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.application.response.MovieTimeListResponse;
|
||||||
|
import com.ticketing.server.movie.service.interfaces.MovieTimeService;
|
||||||
|
import io.swagger.annotations.Api;
|
||||||
|
import io.swagger.annotations.ApiOperation;
|
||||||
|
import io.swagger.annotations.ApiParam;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/movieTimes")
|
||||||
|
@Api(value = "MovieTime API", tags = {"Movie Time"})
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class MovieTimeController {
|
||||||
|
|
||||||
|
private final MovieTimeService movieTimeService;
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
@ApiOperation(value = "영화 시간표 조회")
|
||||||
|
@Validated
|
||||||
|
public ResponseEntity<MovieTimeListResponse> getMovieTimes(
|
||||||
|
@ApiParam(value = "영화 제목", required = true) @RequestParam String title,
|
||||||
|
@ApiParam(value = "상영 날짜", required = true) @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate runningDate) {
|
||||||
|
return ResponseEntity.status(HttpStatus.OK).body(MovieTimeListResponse.from(movieTimeService.getMovieTimes(title, runningDate)));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.ticketing.server.movie.application;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class SeatController {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.ticketing.server.movie.application;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class TheaterController {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.ticketing.server.movie.application;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.application.response.TicketDetailsResponse;
|
||||||
|
import com.ticketing.server.movie.service.dto.TicketDetailsDTO;
|
||||||
|
import com.ticketing.server.movie.service.interfaces.TicketService;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@RequestMapping("/api/tickets")
|
||||||
|
@Validated
|
||||||
|
public class TicketController {
|
||||||
|
|
||||||
|
private final TicketService ticketService;
|
||||||
|
|
||||||
|
@GetMapping("payments/{paymentId}")
|
||||||
|
public ResponseEntity<TicketDetailsResponse> findTicketsByPaymentId(@PathVariable("paymentId") @NotNull Long paymentId) {
|
||||||
|
TicketDetailsDTO tickets = ticketService.findTicketsByPaymentId(paymentId);
|
||||||
|
return ResponseEntity.status(HttpStatus.OK).body(tickets.toResponse());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ticketing.server.movie.application.request;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.service.dto.MovieRegisterDTO;
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MovieRegisterRequest {
|
||||||
|
|
||||||
|
@NotEmpty(message = "{validation.not.empty.title}")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@NotNull(message = "{validation.not.null.runningTime}")
|
||||||
|
private Long runningTime;
|
||||||
|
|
||||||
|
public MovieRegisterDTO toMovieRegisterDTO() {
|
||||||
|
return new MovieRegisterDTO(this.title, this.runningTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ticketing.server.movie.application.response;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.service.dto.MovieDTO;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class MovieListResponse {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "영화 제목")
|
||||||
|
private List<MovieDTO> movieDtos;
|
||||||
|
|
||||||
|
public static MovieListResponse from(List<MovieDTO> movieDtos) {
|
||||||
|
return new MovieListResponse(movieDtos);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ticketing.server.movie.application.response;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.service.dto.MovieTimeDTO;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class MovieTimeListResponse {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "영화 시간표 정보")
|
||||||
|
private List<MovieTimeDTO> movieTimeDTOS;
|
||||||
|
|
||||||
|
public static MovieTimeListResponse from(List<MovieTimeDTO> movieTimeDtos) {
|
||||||
|
return new MovieTimeListResponse(movieTimeDtos);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ticketing.server.movie.application.response;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.service.dto.MovieDTO;
|
||||||
|
import io.swagger.annotations.ApiModelProperty;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class MovieTitleResponse {
|
||||||
|
|
||||||
|
@ApiModelProperty(value = "영화 제목")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
public static MovieTitleResponse from(MovieDTO movieDto) {
|
||||||
|
return new MovieTitleResponse(movieDto.getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ticketing.server.movie.application.response;
|
||||||
|
|
||||||
|
import com.ticketing.server.payment.service.dto.TicketDetailDTO;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
public class TicketDetailsResponse {
|
||||||
|
|
||||||
|
private final List<TicketDetailDTO> ticketDetails;
|
||||||
|
|
||||||
|
public TicketDetailsResponse(List<TicketDetailDTO> ticketDetails) {
|
||||||
|
this.ticketDetails = ticketDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package com.ticketing.server.movie.domain;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.dto.repository.AbstractEntity;
|
||||||
|
import javax.persistence.Column;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class Movie extends AbstractEntity {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Column(unique = true)
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Long runningTime;
|
||||||
|
|
||||||
|
Movie(Long id, String title, Long runningTime) {
|
||||||
|
this.id = id;
|
||||||
|
this.title = title;
|
||||||
|
this.runningTime = runningTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package com.ticketing.server.movie.domain;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.dto.repository.AbstractEntity;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.persistence.CascadeType;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.FetchType;
|
||||||
|
import javax.persistence.JoinColumn;
|
||||||
|
import javax.persistence.ManyToOne;
|
||||||
|
import javax.persistence.OneToMany;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
public class MovieTime extends AbstractEntity {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "movie_id", referencedColumnName = "id", updatable = false)
|
||||||
|
private Movie movie;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "theater_id", referencedColumnName = "id", updatable = false)
|
||||||
|
private Theater theater;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Integer round;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private LocalDateTime startAt;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private LocalDateTime endAt;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "movieTime", cascade = CascadeType.ALL)
|
||||||
|
private List<Ticket> tickets = new ArrayList<>();
|
||||||
|
|
||||||
|
public MovieTime(Movie movie, Theater theater, int round, LocalDateTime startAt) {
|
||||||
|
this.movie = movie;
|
||||||
|
this.theater = theater;
|
||||||
|
this.round = round;
|
||||||
|
this.startAt = startAt;
|
||||||
|
this.endAt = generateEndAt(startAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
MovieTime(Long id, Movie movie, Theater theater, int round, LocalDateTime startAt) {
|
||||||
|
this.id = id;
|
||||||
|
this.movie = movie;
|
||||||
|
this.theater = theater;
|
||||||
|
this.round = round;
|
||||||
|
this.startAt = startAt;
|
||||||
|
this.endAt = generateEndAt(startAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LocalDateTime generateEndAt(LocalDateTime startAt) {
|
||||||
|
Long runningTime = movie.getRunningTime();
|
||||||
|
return startAt.plusMinutes(runningTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMovieTitle() {
|
||||||
|
return this.movie.getTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Seat> getSeats() {
|
||||||
|
return this.theater.getSeats();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package com.ticketing.server.movie.domain;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.dto.repository.AbstractEntity;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.FetchType;
|
||||||
|
import javax.persistence.JoinColumn;
|
||||||
|
import javax.persistence.ManyToOne;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
public class Seat extends AbstractEntity {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "theater_id", referencedColumnName = "id", updatable = false)
|
||||||
|
private Theater theater;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Integer seatColumn;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Integer seatRow;
|
||||||
|
|
||||||
|
public Seat(Integer seatColumn, Integer seatRow, Theater theater) {
|
||||||
|
this.seatColumn = seatColumn;
|
||||||
|
this.seatRow = seatRow;
|
||||||
|
setTheater(theater);
|
||||||
|
}
|
||||||
|
|
||||||
|
Seat(Long id, int column, int row, Theater theater) {
|
||||||
|
this.id = id;
|
||||||
|
this.seatColumn = column;
|
||||||
|
this.seatRow = row;
|
||||||
|
setTheater(theater);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTheater(Theater theater) {
|
||||||
|
this.theater = theater;
|
||||||
|
theater.addSeat(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTheaterNumber() {
|
||||||
|
return this.theater.getTheaterNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package com.ticketing.server.movie.domain;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.dto.repository.AbstractEntity;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import javax.persistence.CascadeType;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.OneToMany;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
public class Theater extends AbstractEntity {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Integer theaterNumber;
|
||||||
|
|
||||||
|
@OneToMany(mappedBy = "theater", cascade = CascadeType.ALL)
|
||||||
|
private List<Seat> seats = new ArrayList<>();
|
||||||
|
|
||||||
|
Theater(Long id, Integer theaterNumber) {
|
||||||
|
this.id = id;
|
||||||
|
this.theaterNumber = theaterNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Theater(Integer theaterNumber) {
|
||||||
|
this.theaterNumber = theaterNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addSeat(Seat seat) {
|
||||||
|
seats.add(seat);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
package com.ticketing.server.movie.domain;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.dto.repository.AbstractEntity;
|
||||||
|
import com.ticketing.server.global.exception.ErrorCode;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import javax.persistence.Entity;
|
||||||
|
import javax.persistence.EnumType;
|
||||||
|
import javax.persistence.Enumerated;
|
||||||
|
import javax.persistence.FetchType;
|
||||||
|
import javax.persistence.JoinColumn;
|
||||||
|
import javax.persistence.ManyToOne;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||||
|
public class Ticket extends AbstractEntity {
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "seat_id", referencedColumnName = "id", updatable = false)
|
||||||
|
private Seat seat;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "movie_times_id", referencedColumnName = "id", updatable = false)
|
||||||
|
private MovieTime movieTime;
|
||||||
|
|
||||||
|
private Long paymentId;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
private TicketStatus status;
|
||||||
|
|
||||||
|
@NotNull
|
||||||
|
private Integer ticketPrice;
|
||||||
|
|
||||||
|
public Ticket(Seat seat, MovieTime movieTime, Integer ticketPrice) {
|
||||||
|
this.seat = seat;
|
||||||
|
this.movieTime = movieTime;
|
||||||
|
this.ticketPrice = ticketPrice;
|
||||||
|
this.status = TicketStatus.SALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ticket(Long id, Seat seat, MovieTime movieTime, Integer ticketPrice) {
|
||||||
|
this.id = id;
|
||||||
|
this.seat = seat;
|
||||||
|
this.movieTime = movieTime;
|
||||||
|
this.ticketPrice = ticketPrice;
|
||||||
|
this.status = TicketStatus.SALE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getColumn() {
|
||||||
|
return this.seat.getSeatColumn();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getRow() {
|
||||||
|
return this.seat.getSeatRow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getStartAt() {
|
||||||
|
return this.movieTime.getStartAt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public LocalDateTime getEndAt() {
|
||||||
|
return this.movieTime.getEndAt();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getTheaterNumber() {
|
||||||
|
return this.seat.getTheaterNumber();
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMovieTitle() {
|
||||||
|
return this.movieTime.getMovieTitle();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void registerPayment(Long id) {
|
||||||
|
if (this.paymentId != null) {
|
||||||
|
throw ErrorCode.throwDuplicatePayment();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.paymentId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package com.ticketing.server.movie.domain;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public enum TicketStatus {
|
||||||
|
|
||||||
|
SALE("판매가능"),
|
||||||
|
SCHEDULED("환불"),
|
||||||
|
SOLD("판매완료");
|
||||||
|
|
||||||
|
private String name;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ticketing.server.movie.domain.repository;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.domain.Movie;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface MovieRepository extends JpaRepository<Movie, Long> {
|
||||||
|
|
||||||
|
Optional<Movie> findByTitle(String title);
|
||||||
|
|
||||||
|
@Query(value = "SELECT * "
|
||||||
|
+ "FROM movie "
|
||||||
|
+ "WHERE deleted_at IS NULL", nativeQuery = true)
|
||||||
|
List<Movie> findValidMovies();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package com.ticketing.server.movie.domain.repository;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.domain.Movie;
|
||||||
|
import com.ticketing.server.movie.domain.MovieTime;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface MovieTimeRepository extends JpaRepository<MovieTime, Long> {
|
||||||
|
|
||||||
|
@Query(value = "SELECT mt "
|
||||||
|
+ "FROM MovieTime mt "
|
||||||
|
+ "JOIN FETCH mt.movie "
|
||||||
|
+ "WHERE mt.movie = :movie "
|
||||||
|
+ "AND mt.startAt BETWEEN :startOfDay AND :endOfDay ")
|
||||||
|
List<MovieTime> findValidMovieTimes(Movie movie, LocalDateTime startOfDay, LocalDateTime endOfDay);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.ticketing.server.movie.domain.repository;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.domain.Seat;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface SeatRepository extends JpaRepository<Seat, Long> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ticketing.server.movie.domain.repository;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.domain.Theater;
|
||||||
|
import java.util.Optional;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TheaterRepository extends JpaRepository<Theater, Long> {
|
||||||
|
|
||||||
|
Optional<Theater> findByTheaterNumber(Integer theaterNumber);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ticketing.server.movie.domain.repository;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.domain.Ticket;
|
||||||
|
import java.util.List;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public interface TicketRepository extends JpaRepository<Ticket, Long> {
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
value =
|
||||||
|
"SELECT t "
|
||||||
|
+ "FROM Ticket t "
|
||||||
|
+ "JOIN FETCH t.movieTime mt "
|
||||||
|
+ "JOIN FETCH t.seat s "
|
||||||
|
+ "JOIN FETCH s.theater th "
|
||||||
|
+ "WHERE t.paymentId = :paymentId "
|
||||||
|
)
|
||||||
|
List<Ticket> findTicketFetchJoinByPaymentId(@Param("paymentId") Long paymentId);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.ticketing.server.movie.service;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.exception.ErrorCode;
|
||||||
|
import com.ticketing.server.movie.domain.Movie;
|
||||||
|
import com.ticketing.server.movie.domain.repository.MovieRepository;
|
||||||
|
import com.ticketing.server.movie.service.dto.MovieDTO;
|
||||||
|
import com.ticketing.server.movie.service.dto.MovieRegisterDTO;
|
||||||
|
import com.ticketing.server.movie.service.interfaces.MovieService;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class MovieServiceImpl implements MovieService {
|
||||||
|
|
||||||
|
private final MovieRepository movieRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MovieDTO registerMovie(MovieRegisterDTO movieRegisterDto) {
|
||||||
|
Optional<Movie> movie = movieRepository.findByTitle(movieRegisterDto.getTitle());
|
||||||
|
|
||||||
|
if(movie.isEmpty()) {
|
||||||
|
return MovieDTO.from(movieRepository.save(movieRegisterDto.toMovie()));
|
||||||
|
}
|
||||||
|
|
||||||
|
throw ErrorCode.throwDuplicateMovie();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MovieDTO> getMovies() {
|
||||||
|
List<Movie> movies = movieRepository.findValidMovies();
|
||||||
|
|
||||||
|
return movies.stream()
|
||||||
|
.map(MovieDTO::from)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.ticketing.server.movie.service;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.exception.ErrorCode;
|
||||||
|
import com.ticketing.server.movie.domain.Movie;
|
||||||
|
import com.ticketing.server.movie.domain.MovieTime;
|
||||||
|
import com.ticketing.server.movie.domain.repository.MovieRepository;
|
||||||
|
import com.ticketing.server.movie.domain.repository.MovieTimeRepository;
|
||||||
|
import com.ticketing.server.movie.service.dto.MovieTimeDTO;
|
||||||
|
import com.ticketing.server.movie.service.interfaces.MovieTimeService;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Slf4j
|
||||||
|
public class MovieTimeServiceImpl implements MovieTimeService {
|
||||||
|
|
||||||
|
private final MovieRepository movieRepository;
|
||||||
|
|
||||||
|
private final MovieTimeRepository movieTimeRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MovieTimeDTO> getMovieTimes(String title, LocalDate runningDate) {
|
||||||
|
Movie movie = movieRepository.findByTitle(title)
|
||||||
|
.orElseThrow(ErrorCode::throwMovieNotFound);
|
||||||
|
|
||||||
|
LocalDateTime startOfDay = runningDate.atStartOfDay().plusHours(6);
|
||||||
|
LocalDateTime endOfDay = startOfDay.plusDays(1);
|
||||||
|
|
||||||
|
List<MovieTime> movieTimes = movieTimeRepository.findValidMovieTimes(movie, startOfDay, endOfDay);
|
||||||
|
|
||||||
|
return movieTimes.stream()
|
||||||
|
.map(MovieTimeDTO::from)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.ticketing.server.movie.service;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.service.interfaces.SeatService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class SeatServiceImpl implements SeatService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package com.ticketing.server.movie.service;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.service.interfaces.TheaterService;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class TheaterServiceImpl implements TheaterService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.ticketing.server.movie.service;
|
||||||
|
|
||||||
|
import com.ticketing.server.global.exception.ErrorCode;
|
||||||
|
import com.ticketing.server.movie.domain.repository.TicketRepository;
|
||||||
|
import com.ticketing.server.movie.service.dto.TicketDetailsDTO;
|
||||||
|
import com.ticketing.server.movie.service.interfaces.TicketService;
|
||||||
|
import com.ticketing.server.payment.service.dto.TicketDetailDTO;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
@Validated
|
||||||
|
@Slf4j
|
||||||
|
public class TicketServiceImpl implements TicketService {
|
||||||
|
|
||||||
|
private final TicketRepository ticketRepository;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TicketDetailsDTO findTicketsByPaymentId(@NotNull Long paymentId) {
|
||||||
|
List<TicketDetailDTO> ticketDetails = ticketRepository.findTicketFetchJoinByPaymentId(paymentId)
|
||||||
|
.stream()
|
||||||
|
.map(TicketDetailDTO::new)
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (ticketDetails.isEmpty()) {
|
||||||
|
throw ErrorCode.throwPaymentIdNotFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TicketDetailsDTO(ticketDetails);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ticketing.server.movie.service.dto;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.domain.Movie;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class MovieDTO {
|
||||||
|
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
public static MovieDTO from(Movie movie) {
|
||||||
|
return new MovieDTO(movie.getTitle());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.ticketing.server.movie.service.dto;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.domain.Movie;
|
||||||
|
import javax.validation.constraints.NotEmpty;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class MovieRegisterDTO {
|
||||||
|
|
||||||
|
@NotEmpty(message = "{validation.not.empty.title}")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@NotNull(message = "{validation.not.null.runningTime}")
|
||||||
|
private Long runningTime;
|
||||||
|
|
||||||
|
public Movie toMovie() {
|
||||||
|
return new Movie(this.title, this.runningTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ticketing.server.movie.service.dto;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.domain.MovieTime;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class MovieTimeDTO {
|
||||||
|
|
||||||
|
private Long movieTimeId;
|
||||||
|
|
||||||
|
private Integer theaterNumber;
|
||||||
|
|
||||||
|
private Integer round;
|
||||||
|
|
||||||
|
private LocalDateTime startAt;
|
||||||
|
|
||||||
|
private LocalDateTime endAt;
|
||||||
|
|
||||||
|
public static MovieTimeDTO from(MovieTime movieTime) {
|
||||||
|
return new MovieTimeDTO(movieTime.getId(), movieTime.getTheater().getTheaterNumber(),
|
||||||
|
movieTime.getRound(), movieTime.getStartAt(), movieTime.getEndAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.ticketing.server.movie.service.dto;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.application.response.TicketDetailsResponse;
|
||||||
|
import com.ticketing.server.payment.service.dto.TicketDetailDTO;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TicketDetailsDTO {
|
||||||
|
|
||||||
|
private final List<TicketDetailDTO> ticketDetails;
|
||||||
|
|
||||||
|
public TicketDetailsResponse toResponse() {
|
||||||
|
return new TicketDetailsResponse(ticketDetails);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ticketing.server.movie.service.interfaces;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.service.dto.MovieDTO;
|
||||||
|
import com.ticketing.server.movie.service.dto.MovieRegisterDTO;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface MovieService {
|
||||||
|
|
||||||
|
MovieDTO registerMovie(MovieRegisterDTO movieRegisterDto);
|
||||||
|
|
||||||
|
List<MovieDTO> getMovies();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package com.ticketing.server.movie.service.interfaces;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.service.dto.MovieTimeDTO;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface MovieTimeService {
|
||||||
|
|
||||||
|
List<MovieTimeDTO> getMovieTimes(String title, LocalDate runningDate);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.ticketing.server.movie.service.interfaces;
|
||||||
|
|
||||||
|
public interface SeatService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.ticketing.server.movie.service.interfaces;
|
||||||
|
|
||||||
|
public interface TheaterService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.ticketing.server.movie.service.interfaces;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.service.dto.TicketDetailsDTO;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public interface TicketService {
|
||||||
|
|
||||||
|
TicketDetailsDTO findTicketsByPaymentId(@NotNull Long paymentId);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.ticketing.server.movie.setup;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.context.annotation.Profile;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@Profile(value = {"local"})
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class MovieSetupController {
|
||||||
|
|
||||||
|
private final MovieSetupService movieSetupService;
|
||||||
|
|
||||||
|
@PostConstruct
|
||||||
|
public void setup() {
|
||||||
|
movieSetupService.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.ticketing.server.movie.setup;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.domain.Movie;
|
||||||
|
import com.ticketing.server.movie.domain.MovieTime;
|
||||||
|
import com.ticketing.server.movie.domain.Seat;
|
||||||
|
import com.ticketing.server.movie.domain.Theater;
|
||||||
|
import com.ticketing.server.movie.domain.Ticket;
|
||||||
|
import com.ticketing.server.movie.domain.repository.MovieRepository;
|
||||||
|
import com.ticketing.server.movie.domain.repository.MovieTimeRepository;
|
||||||
|
import com.ticketing.server.movie.domain.repository.TheaterRepository;
|
||||||
|
import com.ticketing.server.movie.domain.repository.TicketRepository;
|
||||||
|
import com.ticketing.server.payment.domain.Payment;
|
||||||
|
import com.ticketing.server.payment.domain.PaymentStatus;
|
||||||
|
import com.ticketing.server.payment.domain.PaymentType;
|
||||||
|
import com.ticketing.server.payment.domain.repository.PaymentRepository;
|
||||||
|
import com.ticketing.server.payment.service.dto.CreatePaymentDTO;
|
||||||
|
import com.ticketing.server.user.domain.User;
|
||||||
|
import com.ticketing.server.user.domain.UserGrade;
|
||||||
|
import com.ticketing.server.user.domain.repository.UserRepository;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class
|
||||||
|
MovieSetupService {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final MovieRepository movieRepository;
|
||||||
|
private final MovieTimeRepository movieTimeRepository;
|
||||||
|
private final TheaterRepository theaterRepository;
|
||||||
|
private final TicketRepository ticketRepository;
|
||||||
|
private final PaymentRepository paymentRepository;
|
||||||
|
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void init() {
|
||||||
|
initMovie();
|
||||||
|
initTheater();
|
||||||
|
initMovieTime();
|
||||||
|
initTicket();
|
||||||
|
initPayment();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initMovie() {
|
||||||
|
List<Movie> movies = Arrays.asList(
|
||||||
|
new Movie("탑건: 매버릭", 130L),
|
||||||
|
new Movie("헤어질 결심", 138L),
|
||||||
|
new Movie("마녀2", 137L),
|
||||||
|
new Movie("범죄도시2", 106L),
|
||||||
|
new Movie("버즈 라이트이어", 105L)
|
||||||
|
);
|
||||||
|
|
||||||
|
movieRepository.saveAll(movies);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initTheater() {
|
||||||
|
List<Theater> theaters = Arrays.asList(
|
||||||
|
new Theater(1),
|
||||||
|
new Theater(2)
|
||||||
|
);
|
||||||
|
|
||||||
|
for (Theater theater : theaters) {
|
||||||
|
for (int col = 1; col <= 2; col++) {
|
||||||
|
for (int row = 1; row <= 10; row++) {
|
||||||
|
new Seat(col, row, theater);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
theaterRepository.saveAll(theaters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initMovieTime() {
|
||||||
|
List<Movie> movies = movieRepository.findAll();
|
||||||
|
List<Theater> theaters = theaterRepository.findAll();
|
||||||
|
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
|
||||||
|
List<MovieTime> movieTimes = new ArrayList<>();
|
||||||
|
for (Theater theater : theaters) {
|
||||||
|
movieTimes.add(new MovieTime(movies.get(0), theater, 1, LocalDateTime.of(now.getYear(), now.getMonthValue(), now.getDayOfMonth(), 8, 0)));
|
||||||
|
movieTimes.add(new MovieTime(movies.get(0), theater, 3, LocalDateTime.of(now.getYear(), now.getMonthValue(), now.getDayOfMonth(), 12, 0)));
|
||||||
|
movieTimes.add(new MovieTime(movies.get(1), theater, 2, LocalDateTime.of(now.getYear(), now.getMonthValue(), now.getDayOfMonth(), 10, 0)));
|
||||||
|
movieTimes.add(new MovieTime(movies.get(2), theater, 4, LocalDateTime.of(now.getYear(), now.getMonthValue(), now.getDayOfMonth(), 14, 0)));
|
||||||
|
movieTimes.add(new MovieTime(movies.get(0), theater, 5, LocalDateTime.of(now.getYear(), now.getMonthValue(), now.getDayOfMonth(), 16, 0)));
|
||||||
|
movieTimes.add(new MovieTime(movies.get(3), theater, 6, LocalDateTime.of(now.getYear(), now.getMonthValue(), now.getDayOfMonth(), 18, 0)));
|
||||||
|
movieTimes.add(new MovieTime(movies.get(0), theater, 7, LocalDateTime.of(now.getYear(), now.getMonthValue(), now.getDayOfMonth(), 21, 0)));
|
||||||
|
movieTimes.add(new MovieTime(movies.get(4), theater, 8, LocalDateTime.of(now.getYear(), now.getMonthValue(), now.getDayOfMonth(), 23, 0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
movieTimeRepository.saveAll(movieTimes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initTicket() {
|
||||||
|
List<MovieTime> movieTimes = movieTimeRepository.findAll();
|
||||||
|
|
||||||
|
List<Ticket> tickets = new ArrayList<>();
|
||||||
|
Integer ticketPrice = 15_000;
|
||||||
|
for (MovieTime movieTime : movieTimes) {
|
||||||
|
for (Seat seat : movieTime.getSeats()) {
|
||||||
|
tickets.add(new Ticket(seat, movieTime, ticketPrice));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ticketRepository.saveAll(tickets);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initPayment() {
|
||||||
|
User user = userRepository.save(new User(123L, "김동효", "kdhyo98@gmail.com", passwordEncoder.encode("123123"), UserGrade.USER, "010-1234-5678"));
|
||||||
|
|
||||||
|
List<Ticket> tickets = ticketRepository.findAll();
|
||||||
|
Ticket ticket = tickets.get(0);
|
||||||
|
String title = ticket.getMovieTime().getMovie().getTitle();
|
||||||
|
CreatePaymentDTO createPaymentDto = new CreatePaymentDTO(user.getAlternateId(), title, PaymentType.KAKAO_PAY, PaymentStatus.COMPLETED, "2022-0710-4142", 30_000);
|
||||||
|
Payment payment = createPaymentDto.toEntity();
|
||||||
|
|
||||||
|
paymentRepository.save(payment);
|
||||||
|
|
||||||
|
ticket.registerPayment(payment.getId());
|
||||||
|
tickets.get(1).registerPayment(payment.getId());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.ticketing.server.payment.api;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.application.response.TicketDetailsResponse;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
public interface MovieClient {
|
||||||
|
|
||||||
|
TicketDetailsResponse getTicketsByPaymentId(@NotNull Long paymentId);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package com.ticketing.server.payment.api;
|
||||||
|
|
||||||
|
import com.ticketing.server.payment.api.dto.response.UserDetailResponse;
|
||||||
|
|
||||||
|
public interface UserClient {
|
||||||
|
|
||||||
|
UserDetailResponse detail();
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package com.ticketing.server.payment.api.dto.response;
|
||||||
|
|
||||||
|
import com.ticketing.server.payment.domain.Payment;
|
||||||
|
import com.ticketing.server.user.domain.UserGrade;
|
||||||
|
import com.ticketing.server.user.service.dto.UserDetailDTO;
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@AllArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class UserDetailResponse {
|
||||||
|
|
||||||
|
private final Long userAlternateId;
|
||||||
|
private final String name;
|
||||||
|
private final String email;
|
||||||
|
private final UserGrade grade;
|
||||||
|
private final String phone;
|
||||||
|
|
||||||
|
public UserDetailResponse(UserDetailDTO userDetailDto) {
|
||||||
|
this(
|
||||||
|
userDetailDto.getAlternateId(),
|
||||||
|
userDetailDto.getName(),
|
||||||
|
userDetailDto.getEmail(),
|
||||||
|
userDetailDto.getGrade(),
|
||||||
|
userDetailDto.getPhone()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasUserAlternateId(Payment payment) {
|
||||||
|
return userAlternateId.equals(payment.getUserAlternateId());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.ticketing.server.payment.api.impl;
|
||||||
|
|
||||||
|
import com.ticketing.server.movie.application.response.TicketDetailsResponse;
|
||||||
|
import com.ticketing.server.movie.service.dto.TicketDetailsDTO;
|
||||||
|
import com.ticketing.server.movie.service.interfaces.TicketService;
|
||||||
|
import com.ticketing.server.payment.api.MovieClient;
|
||||||
|
import javax.validation.constraints.NotNull;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Validated
|
||||||
|
public class MovieClientImpl implements MovieClient {
|
||||||
|
|
||||||
|
private final TicketService ticketService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TicketDetailsResponse getTicketsByPaymentId(@NotNull Long paymentId) {
|
||||||
|
TicketDetailsDTO ticketDetails = ticketService.findTicketsByPaymentId(paymentId);
|
||||||
|
return ticketDetails.toResponse();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.ticketing.server.payment.api.impl;
|
||||||
|
|
||||||
|
import com.ticketing.server.payment.api.UserClient;
|
||||||
|
import com.ticketing.server.payment.api.dto.response.UserDetailResponse;
|
||||||
|
import com.ticketing.server.user.service.dto.UserDetailDTO;
|
||||||
|
import com.ticketing.server.user.service.interfaces.UserService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserClientImpl implements UserClient {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetailResponse detail() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
UserDetails details = (UserDetails) authentication.getPrincipal();
|
||||||
|
|
||||||
|
UserDetailDTO userDetail = userService.findDetailByEmail(details.getUsername());
|
||||||
|
return new UserDetailResponse(userDetail);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user