diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..b648bc0 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,58 @@ +{ + "permissions": { + "allow": [ + "Bash(./gradlew test:*)", + "Bash(./gradlew clean build:*)", + "Bash(./gradlew spotlessApply)", + "Read(//c/Program Files/Eclipse Adoptium/**)", + "Bash(export JAVA_HOME=\"C:/Program Files/Eclipse Adoptium/jdk-25.0.0.36-hotspot\")", + "Bash(where java)", + "Bash(curl -L \"https://api.adoptium.net/v3/binary/latest/21/ga/windows/x64/jdk/hotspot/normal/eclipse\" -o /tmp/jdk21.zip)", + "Read(//c/**)", + "Bash(export JAVA_HOME=\"/tmp/jdk-21.0.8+9\")", + "Bash(export JWT_SECRET_KEY=\"test-secret-key-for-migration-only\")", + "Bash(export JWT_ACCESS_TOKEN_VALIDITY=\"3600\")", + "Bash(export JWT_REFRESH_TOKEN_VALIDITY=\"86400\")", + "Bash(timeout 30 ./gradlew bootRun)", + "Bash(timeout 30 ./gradlew bootRun --info)", + "Bash(export JAVA_HOME=\"C:/Users/Desktop/.jdks/corretto-21.0.6\")", + "Bash(tee /tmp/bootrun.log)", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"SELECT id, search_option_name, display_order, is_active FROM auction_search_option_metadata ORDER BY display_order LIMIT 5;\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"SELECT search_option_name, JSON_PRETTY(search_condition_json) as json_data FROM auction_search_option_metadata WHERE id = 1;\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"SELECT COUNT(*) as total_count FROM auction_search_option_metadata; SELECT id, search_option_name, display_order FROM auction_search_option_metadata ORDER BY display_order;\")", + "Bash(curl -X GET http://localhost:8092/api/search-option -H \"Content-Type: application/json\")", + "Bash(python -m json.tool)", + "Bash(netstat -ano)", + "Bash(awk '{print $5}')", + "Bash(xargs -I {} taskkill //PID {} //F)", + "Bash(export JWT_SECRET_KEY=\"test-secret-key\")", + "Bash(timeout 40 ./gradlew bootRun)", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"\nEXPLAIN\nSELECT *\nFROM auction_history ah\nINNER JOIN auction_item_option io ON ah.auction_buy_id = io.auction_history_id\nWHERE ah.item_top_category = ''근거리 장비''\n AND ah.item_sub_category = ''검''\n AND ah.item_name = ''고스트 소드''\n AND ah.auction_price_per_unit BETWEEN 10 AND 1000000000\n AND ah.date_auction_buy BETWEEN ''2023-01-01'' AND ''2025-12-31''\n AND ((io.option_type = ''세공 옵션'' AND io.option_value2 >= 2)\n OR (io.option_type = ''공격'' AND io.option_value2 >= 2)\n OR (io.option_type = ''인챈트'' AND io.option_sub_type = ''접미'' AND io.option_value = ''다이어 울프''))\nLIMIT 10;\n\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"\nEXPLAIN\nSELECT *\nFROM auction_history ah\nINNER JOIN auction_item_option io ON ah.auction_buy_id = io.auction_history_id\nWHERE ah.item_top_category = ''근거리 장비''\n AND ah.item_sub_category = ''검''\n AND ah.item_name = ''고스트 소드''\n AND ah.auction_price_per_unit BETWEEN 10 AND 1000000000\n AND ah.date_auction_buy BETWEEN ''2023-01-01'' AND ''2025-12-31''\n AND ah.auction_buy_id IN (\n SELECT io2.auction_history_id \n FROM auction_item_option io2 \n WHERE ((io2.option_type = ''세공 옵션'' AND io2.option_value2 >= 2)\n OR (io2.option_type = ''공격'' AND io2.option_value2 >= 2)\n OR (io2.option_type = ''인챈트'' AND io2.option_sub_type = ''접미'' AND io2.option_value = ''다이어 울프''))\n )\nLIMIT 10;\n\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"\nSELECT item_top_category, item_sub_category, item_name, COUNT(*) as cnt\nFROM auction_history\nGROUP BY item_top_category, item_sub_category, item_name\nORDER BY cnt DESC\nLIMIT 5;\n\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"\nDESCRIBE auction_item_option;\n\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"\nSELECT option_type, option_sub_type, COUNT(*) as cnt\nFROM auction_item_option\nGROUP BY option_type, option_sub_type\nORDER BY cnt DESC\nLIMIT 10;\n\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"\nEXPLAIN FORMAT=TREE\nSELECT ah.*, io.*\nFROM auction_history ah\nINNER JOIN auction_item_option io ON ah.auction_buy_id = io.auction_history_id\nWHERE ah.item_top_category = ''소모품''\n AND ((io.option_type = ''공격'' AND io.option_value2 >= ''2'')\n OR (io.option_type = ''밸런스'' AND io.option_value2 >= ''5''))\nLIMIT 10;\n\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"\nSELECT ah.item_top_category, COUNT(DISTINCT ah.auction_buy_id) as history_cnt, COUNT(io.id) as option_cnt\nFROM auction_history ah\nLEFT JOIN auction_item_option io ON ah.auction_buy_id = io.auction_history_id\nGROUP BY ah.item_top_category\nORDER BY option_cnt DESC\nLIMIT 5;\n\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"\n-- Query 1: Direct JOIN with WHERE\nEXPLAIN FORMAT=TREE\nSELECT ah.auction_buy_id, ah.item_name, io.option_type, io.option_value2\nFROM auction_history ah\nINNER JOIN auction_item_option io ON ah.auction_buy_id = io.auction_history_id\nWHERE ah.item_top_category = ''근거리 장비''\n AND ((io.option_type = ''공격'' AND CAST(io.option_value2 AS UNSIGNED) >= 2)\n OR (io.option_type = ''밸런스'' AND CAST(io.option_value2 AS UNSIGNED) >= 5))\nLIMIT 10;\n\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"\n-- Check if JOIN works\nSELECT \n ah.auction_buy_id,\n ah.item_name,\n ah.item_top_category,\n io.option_type,\n io.option_value2\nFROM auction_history ah\nINNER JOIN auction_item_option io ON ah.auction_buy_id = io.auction_history_id\nWHERE ah.item_top_category = ''근거리 장비''\nLIMIT 5;\n\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"\n-- Check auction_buy_id samples from both tables\nSELECT ''auction_history'' as source, auction_buy_id FROM auction_history LIMIT 3\nUNION ALL\nSELECT ''auction_item_option'' as source, auction_history_id FROM auction_item_option WHERE auction_history_id IS NOT NULL LIMIT 3;\n\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"SELECT auction_buy_id FROM auction_history LIMIT 3;\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"SELECT auction_history_id FROM auction_item_option WHERE auction_history_id IS NOT NULL LIMIT 3;\")", + "Bash(mysql -h 127.0.0.1 -P 3318 -u devnogi -pdevnogi0529! devnogi -e \"\nEXPLAIN FORMAT=JSON\nSELECT ah.auction_buy_id, ah.item_name, ah.item_top_category, io.option_type, io.option_value2\nFROM auction_history ah\nINNER JOIN auction_item_option io ON ah.auction_buy_id = io.auction_history_id\nWHERE ah.item_top_category = ''''근거리 장비''''\n AND ((io.option_type = ''''공격'''')\n OR (io.option_type = ''''밸런스''''))\nLIMIT 10;\n\")", + "Bash(cat)", + "Bash(./gradlew clean test:*)", + "Bash(cat:*)", + "Bash(unset JAVA_HOME)", + "Bash(./gradlew:*)", + "Bash(export PATH=\"/c/Program Files/Eclipse Adoptium/jdk-25.0.0.36-hotspot/bin:$PATH\":*)", + "Bash(java:*)", + "Bash(curl:*)", + "Bash(unzip:*)", + "Bash(export PATH=\"$JAVA_HOME/bin:$PATH\")", + "Bash(mysql:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/error.log b/error.log new file mode 100644 index 0000000..8581937 --- /dev/null +++ b/error.log @@ -0,0 +1,83 @@ +15:30:05,173 |-INFO in ch.qos.logback.classic.LoggerContext[default] - This is logback-classic version 1.5.18 +15:30:05,184 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - Here is a list of configurators discovered as a service, by rank: +15:30:05,184 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - org.springframework.boot.logging.logback.RootLogLevelConfigurator +15:30:05,184 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - They will be invoked in order until ExecutionStatus.DO_NOT_INVOKE_NEXT_IF_ANY is returned. +15:30:05,184 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - Constructed configurator of type class org.springframework.boot.logging.logback.RootLogLevelConfigurator +15:30:05,270 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - org.springframework.boot.logging.logback.RootLogLevelConfigurator.configure() call lasted 1 milliseconds. ExecutionStatus=INVOKE_NEXT_IF_ANY +15:30:05,270 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - Trying to configure with ch.qos.logback.classic.joran.SerializedModelConfigurator +15:30:05,275 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - Constructed configurator of type class ch.qos.logback.classic.joran.SerializedModelConfigurator +15:30:05,286 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.scmo] +15:30:05,364 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.scmo] +15:30:05,364 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - ch.qos.logback.classic.joran.SerializedModelConfigurator.configure() call lasted 89 milliseconds. ExecutionStatus=INVOKE_NEXT_IF_ANY +15:30:05,364 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - Trying to configure with ch.qos.logback.classic.util.DefaultJoranConfigurator +15:30:05,366 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - Constructed configurator of type class ch.qos.logback.classic.util.DefaultJoranConfigurator +15:30:05,368 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback-test.xml] +15:30:05,369 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Could NOT find resource [logback.xml] +15:30:05,369 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - ch.qos.logback.classic.util.DefaultJoranConfigurator.configure() call lasted 3 milliseconds. ExecutionStatus=INVOKE_NEXT_IF_ANY +15:30:05,370 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - Trying to configure with ch.qos.logback.classic.BasicConfigurator +15:30:05,375 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - Constructed configurator of type class ch.qos.logback.classic.BasicConfigurator +15:30:05,375 |-INFO in ch.qos.logback.classic.BasicConfigurator@76f7d241 - Setting up default configuration. +15:30:05,770 |-INFO in ch.qos.logback.core.ConsoleAppender[console] - BEWARE: Writing to the console can be very slow. Avoid logging to the +15:30:05,770 |-INFO in ch.qos.logback.core.ConsoleAppender[console] - console in production environments, especially in high volume systems. +15:30:05,770 |-INFO in ch.qos.logback.core.ConsoleAppender[console] - See also https://logback.qos.ch/codes.html#slowConsole +15:30:05,772 |-INFO in ch.qos.logback.classic.util.ContextInitializer@2783717b - ch.qos.logback.classic.BasicConfigurator.configure() call lasted 397 milliseconds. ExecutionStatus=NEUTRAL +15:30:10,275 |-INFO in ConfigurationWatchList(mainURL=jar:nested:/app.jar/!BOOT-INF/classes/!/logback/logback-display.xml, fileWatchList={}, urlWatchList=[}) - URL [jar:nested:/app.jar/!BOOT-INF/classes/!/logback/logback-display.xml] is not of type file +15:30:10,582 |-WARN in org.springframework.boot.logging.logback.SpringProfileIfNestedWithinSecondPhaseElementSanityChecker@3829ac1 - elements cannot be nested within an , or element +15:30:10,582 |-WARN in org.springframework.boot.logging.logback.SpringProfileIfNestedWithinSecondPhaseElementSanityChecker@3829ac1 - Element at line 5 contains a nested element at line 6 +15:30:10,582 |-WARN in org.springframework.boot.logging.logback.SpringProfileIfNestedWithinSecondPhaseElementSanityChecker@3829ac1 - Element at line 5 contains a nested element at line 9 +15:30:10,582 |-WARN in org.springframework.boot.logging.logback.SpringProfileIfNestedWithinSecondPhaseElementSanityChecker@3829ac1 - Element at line 5 contains a nested element at line 14 +15:30:10,872 |-INFO in ch.qos.logback.core.joran.util.ConfigurationWatchListUtil@4baf352a - Adding [jar:nested:/app.jar/!BOOT-INF/classes/!/logback/logback-appender.xml] to configuration watch list.15:30:10,873 |-INFO in ConfigurationWatchList(mainURL=jar:nested:/app.jar/!BOOT-INF/classes/!/logback/logback-display.xml, fileWatchList={}, urlWatchList=[}) - Cannot watch [jar:nested:/app.jar/!BOOT-INF/classes/!/logback/logback-appender.xml] as its protocol is not one of file, http or https. +15:30:10,883 |-INFO in ch.qos.logback.core.model.processor.ConversionRuleModelHandler - registering conversion word clr with class [org.springframework.boot.logging.logback.ColorConverter] +15:30:10,884 |-INFO in ch.qos.logback.core.model.processor.ConversionRuleModelHandler - registering conversion word wEx with class [org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter] +15:30:10,884 |-INFO in ch.qos.logback.core.model.processor.TimestampModelHandler - Using current interpretation time, i.e. now, as time reference. +15:30:10,886 |-INFO in ch.qos.logback.core.model.processor.TimestampModelHandler - Adding property to the context with key="BY_DATE" and value="2025-10-20" to the LOCAL scope +15:30:10,975 |-INFO in ch.qos.logback.core.model.processor.ModelInterpretationContext@1bb1fde8 - value "[%d{yyyy-MM-dd HH:mm:ss.SSS, Asia/Seoul}] [%5p] [%15.15thread] [%-40.40logger{39}] - %m%n" substituted for "[%d{yyyy-MM-dd HH:mm:ss.SSS, ${logback.timezone:-Asia/Seoul}}] [%5p] [%15.15thread] [%-40.40logger{39}] - %m%n" +15:30:10,977 |-INFO in ch.qos.logback.core.model.processor.AppenderModelHandler - Processing appender named [CONSOLE] +15:30:10,978 |-INFO in ch.qos.logback.core.model.processor.AppenderModelHandler - About to instantiate appender of type [ch.qos.logback.core.ConsoleAppender] +15:30:11,089 |-INFO in ch.qos.logback.core.model.processor.ModelInterpretationContext@1bb1fde8 - value "[%d{yyyy-MM-dd HH:mm:ss.SSS, Asia/Seoul}] [%5p] [%15.15thread] [%-40.40logger{39}] - %m%n" substituted for "${CONSOLE_LOG_PATTERN}" +15:30:11,091 |-WARN in ch.qos.logback.core.model.processor.ImplicitModelHandler - Ignoring unknown property [charset] in [ch.qos.logback.classic.PatternLayout] +15:30:11,174 |-INFO in ch.qos.logback.classic.pattern.DateConverter@15eebbff - Setting zoneId to "Asia/Seoul" +15:30:11,175 |-WARN in ch.qos.logback.core.ConsoleAppender[CONSOLE] - This appender no longer admits a layout as a sub-component, set an encoder instead. +15:30:11,175 |-WARN in ch.qos.logback.core.ConsoleAppender[CONSOLE] - To ensure compatibility, wrapping your layout in LayoutWrappingEncoder. +15:30:11,175 |-WARN in ch.qos.logback.core.ConsoleAppender[CONSOLE] - See also https://logback.qos.ch/codes.html#layoutInsteadOfEncoder for details +15:30:11,175 |-INFO in ch.qos.logback.core.ConsoleAppender[CONSOLE] - BEWARE: Writing to the console can be very slow. Avoid logging to the +15:30:11,175 |-INFO in ch.qos.logback.core.ConsoleAppender[CONSOLE] - console in production environments, especially in high volume systems. +15:30:11,175 |-INFO in ch.qos.logback.core.ConsoleAppender[CONSOLE] - See also https://logback.qos.ch/codes.html#slowConsole +15:30:11,175 |-INFO in ch.qos.logback.core.model.processor.AppenderModelHandler - Processing appender named [FILE] +15:30:11,176 |-INFO in ch.qos.logback.core.model.processor.AppenderModelHandler - About to instantiate appender of type [ch.qos.logback.core.rolling.RollingFileAppender] +15:30:11,181 |-INFO in ch.qos.logback.core.model.processor.ModelInterpretationContext@1bb1fde8 - value "./logs/2025-10-20_log_data.log" substituted for "${LOG_FILE_PATH}/${BY_DATE}_${LOG_FILE_NAME}.log" +15:30:11,182 |-INFO in ch.qos.logback.core.model.processor.ImplicitModelHandler - Assuming default type [ch.qos.logback.classic.encoder.PatternLayoutEncoder] for [encoder] property +15:30:11,182 |-INFO in ch.qos.logback.core.model.processor.ModelInterpretationContext@1bb1fde8 - value "[%d{yyyy-MM-dd HH:mm:ss.SSS, Asia/Seoul}] [%5p] [%15.15thread] [%-40.40logger{39}] - %m%n" substituted for "${CONSOLE_LOG_PATTERN}" +15:30:11,183 |-INFO in ch.qos.logback.classic.pattern.DateConverter@22d6f11 - Setting zoneId to "Asia/Seoul" +15:30:11,263 |-INFO in ch.qos.logback.core.model.processor.ModelInterpretationContext@1bb1fde8 - value "./logs/archive/%d{yyyy-MM-dd, Asia/Seoul}_%i.log" substituted for "${ARCHIVE_FILE_PATH}/%d{yyyy-MM-dd, ${logback.timezone:-Asia/Seoul}}_%i.log" +15:30:11,271 |-INFO in c.q.l.core.rolling.SizeAndTimeBasedRollingPolicy@815336475 - setting totalSizeCap to 50 MB +15:30:11,276 |-INFO in c.q.l.core.rolling.SizeAndTimeBasedRollingPolicy@815336475 - Archive files will be limited to [50 MB] each. +15:30:11,276 |-INFO in c.q.l.core.rolling.SizeAndTimeBasedRollingPolicy@815336475 - No compression will be used +15:30:11,282 |-INFO in c.q.l.core.rolling.SizeAndTimeBasedRollingPolicy@815336475 - Will use the pattern ./logs/archive/%d{yyyy-MM-dd, Asia/Seoul}_%i.log for the active file +15:30:11,486 |-INFO in ch.qos.logback.core.rolling.SizeAndTimeBasedFileNamingAndTriggeringPolicy@2453f95d - The date pattern is 'yyyy-MM-dd' from file name pattern './logs/archive/%d{yyyy-MM-dd, Asia/Seoul}_%i.log'. +15:30:11,486 |-INFO in ch.qos.logback.core.rolling.SizeAndTimeBasedFileNamingAndTriggeringPolicy@2453f95d - Roll-over at midnight. +15:30:11,573 |-INFO in ch.qos.logback.core.rolling.SizeAndTimeBasedFileNamingAndTriggeringPolicy@2453f95d - Setting initial period to 2025-10-20T15:30:11.572Z +15:30:11,582 |-INFO in ch.qos.logback.core.rolling.RollingFileAppender[FILE] - Active log file name: ./logs/2025-10-20_log_data.log +15:30:11,583 |-INFO in ch.qos.logback.core.rolling.RollingFileAppender[FILE] - File property is set to [./logs/2025-10-20_log_data.log] +15:30:11,586 |-INFO in ch.qos.logback.core.model.processor.AppenderModelHandler - Processing appender named [ERROR] +15:30:11,586 |-INFO in ch.qos.logback.core.model.processor.AppenderModelHandler - About to instantiate appender of type [ch.qos.logback.core.rolling.RollingFileAppender] +15:30:11,667 |-INFO in ch.qos.logback.core.model.processor.ModelInterpretationContext@1bb1fde8 - value "./logs/2025-10-20_log_data_error.log" substituted for "${LOG_FILE_PATH}/${BY_DATE}_${ERROR_LOG_FILE_NAME}.log" +15:30:11,668 |-INFO in ch.qos.logback.core.model.processor.ModelInterpretationContext@1bb1fde8 - value "[%d{yyyy-MM-dd HH:mm:ss.SSS, Asia/Seoul}] [%5p] [%15.15thread] [%-40.40logger{39}] - %m%n" substituted for "${CONSOLE_LOG_PATTERN}" +15:30:11,668 |-INFO in ch.qos.logback.classic.pattern.DateConverter@44828f6b - Setting zoneId to "Asia/Seoul" +15:30:11,668 |-INFO in ch.qos.logback.core.model.processor.ModelInterpretationContext@1bb1fde8 - value "./logs/archive/%d{yyyy-MM-dd, Asia/Seoul}_error_%i.log" substituted for "${ARCHIVE_FILE_PATH}/%d{yyyy-MM-dd, ${logback.timezone:-Asia/Seoul}}_error_%i.log" +15:30:11,670 |-INFO in c.q.l.core.rolling.SizeAndTimeBasedRollingPolicy@767436045 - setting totalSizeCap to 50 MB +15:30:11,670 |-INFO in c.q.l.core.rolling.SizeAndTimeBasedRollingPolicy@767436045 - Archive files will be limited to [50 MB] each. +15:30:11,670 |-INFO in c.q.l.core.rolling.SizeAndTimeBasedRollingPolicy@767436045 - No compression will be used +15:30:11,670 |-INFO in c.q.l.core.rolling.SizeAndTimeBasedRollingPolicy@767436045 - Will use the pattern ./logs/archive/%d{yyyy-MM-dd, Asia/Seoul}_error_%i.log for the active file +15:30:11,671 |-INFO in ch.qos.logback.core.rolling.SizeAndTimeBasedFileNamingAndTriggeringPolicy@553f1d75 - The date pattern is 'yyyy-MM-dd' from file name pattern './logs/archive/%d{yyyy-MM-dd, Asia/Seoul}_error_%i.log'. +15:30:11,671 |-INFO in ch.qos.logback.core.rolling.SizeAndTimeBasedFileNamingAndTriggeringPolicy@553f1d75 - Roll-over at midnight. +15:30:11,672 |-INFO in ch.qos.logback.core.rolling.SizeAndTimeBasedFileNamingAndTriggeringPolicy@553f1d75 - Setting initial period to 2025-10-20T15:30:11.672Z +15:30:11,673 |-INFO in ch.qos.logback.core.rolling.RollingFileAppender[ERROR] - Active log file name: ./logs/2025-10-20_log_data_error.log +15:30:11,673 |-INFO in ch.qos.logback.core.rolling.RollingFileAppender[ERROR] - File property is set to [./logs/2025-10-20_log_data_error.log] +15:30:11,674 |-INFO in ch.qos.logback.classic.model.processor.RootLoggerModelHandler - Setting level of ROOT logger to INFO +15:30:11,675 |-INFO in ch.qos.logback.classic.jul.LevelChangePropagator@6e1d8f9e - Propagating INFO level on Logger[ROOT] onto the JUL framework +15:30:11,677 |-INFO in ch.qos.logback.core.model.processor.AppenderRefModelHandler - Attaching appender named [CONSOLE] to Logger[ROOT] +15:30:11,677 |-INFO in ch.qos.logback.core.model.processor.AppenderRefModelHandler - Attaching appender named [FILE] to Logger[ROOT] +15:30:11,677 |-INFO in ch.qos.logback.core.model.processor.AppenderRefModelHandler - Attaching appender named [ERROR] to Logger[ROOT] +15:30:11,677 |-INFO in ch.qos.logback.core.model.processor.DefaultProcessor@3e34ace1 - End of configuration. +15:30:11,679 |-INFO in org.springframework.boot.logging.logback.SpringBootJoranConfigurator@62fe6067 - Registering current configuration as safe fallback point \ No newline at end of file diff --git a/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java b/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java index 8ff81bb..9de88c8 100644 --- a/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java +++ b/src/main/java/until/the/eternity/auctionhistory/application/scheduler/AuctionHistoryScheduler.java @@ -1,10 +1,6 @@ package until.the.eternity.auctionhistory.application.scheduler; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java index c4473a4..3e4cad2 100644 --- a/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java +++ b/src/main/java/until/the/eternity/auctionhistory/infrastructure/persistence/AuctionHistoryQueryDslRepository.java @@ -1,16 +1,32 @@ package until.the.eternity.auctionhistory.infrastructure.persistence; import com.querydsl.core.BooleanBuilder; +import com.querydsl.core.types.Order; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.NumberTemplate; +import com.querydsl.jpa.JPAExpressions; import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.ArrayList; import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.stereotype.Component; import until.the.eternity.auctionhistory.domain.entity.AuctionHistory; import until.the.eternity.auctionhistory.domain.entity.QAuctionHistory; +import until.the.eternity.auctionhistory.interfaces.rest.dto.enums.SearchStandard; import until.the.eternity.auctionhistory.interfaces.rest.dto.request.AuctionHistorySearchRequest; +import until.the.eternity.auctionhistory.interfaces.rest.dto.request.DateAuctionBuyRequest; +import until.the.eternity.auctionhistory.interfaces.rest.dto.request.ItemOptionSearchRequest; +import until.the.eternity.auctionhistory.interfaces.rest.dto.request.PriceSearchRequest; +import until.the.eternity.auctionitemoption.domain.entity.QAuctionItemOption; @Component @RequiredArgsConstructor @@ -18,27 +34,69 @@ class AuctionHistoryQueryDslRepository { private final JPAQueryFactory queryFactory; + /** 옵션 조건 빌드 결과 (조건 BooleanBuilder + 추가된 조건 개수) */ + record OptionConditionResult(BooleanBuilder builder, int count) {} + + /** 경매 거래내역 검색 (옵션 조건 포함) */ public Page search(AuctionHistorySearchRequest condition, Pageable pageable) { QAuctionHistory ah = QAuctionHistory.auctionHistory; - BooleanBuilder builder = buildPredicate(condition, ah); + QAuctionItemOption aio = QAuctionItemOption.auctionItemOption; + + // 1단계: 거래내역 조건 빌드 + BooleanBuilder historyBuilder = buildHistoryPredicate(condition, ah); + + // 2단계: 옵션 조건이 있으면 서브쿼리 추가 + if (condition.itemOptionSearchRequest() != null) { + // 서브쿼리용 별도 QAuctionItemOption 인스턴스 + QAuctionItemOption subOption = new QAuctionItemOption("subOption"); + OptionConditionResult optionResult = + buildItemOptionConditions(condition.itemOptionSearchRequest(), subOption); + // 옵션 조건이 실제로 있는 경우에만 서브쿼리 추가 + if (optionResult.builder().hasValue() && optionResult.count() > 0) { + // 서브쿼리: 옵션 조건을 만족하는 auction_history_id 찾기 + // GROUP BY + HAVING COUNT로 모든 조건을 만족하는 거래내역만 필터링 + var subQuery = + JPAExpressions.select(subOption.auctionHistory.auctionBuyId) + .from(subOption) + .where(optionResult.builder()) + .groupBy(subOption.auctionHistory.auctionBuyId) + .having(subOption.count().eq((long) optionResult.count())); + + // 메인 쿼리에 서브쿼리 결과 적용 + historyBuilder.and(ah.auctionBuyId.in(subQuery)); + } + } + + // 3단계: 정렬 조건 빌드 + List> orderSpecifiers = buildOrderSpecifiers(pageable, ah); + + // 4단계: 모든 옵션과 함께 조회 (LEFT JOIN - 조건 없음!) List content = queryFactory .selectFrom(ah) - .leftJoin(ah.auctionItemOptions) + .leftJoin(ah.auctionItemOptions, aio) .fetchJoin() - .where(builder) + .where(historyBuilder) + .orderBy(orderSpecifiers.toArray(new OrderSpecifier[0])) + .distinct() // 중복 제거 .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); - Long total = queryFactory.select(ah.count()).from(ah).where(builder).fetchOne(); + // Count 쿼리 (JOIN 없이 실행) + Long total = + queryFactory.select(ah.countDistinct()).from(ah).where(historyBuilder).fetchOne(); return new PageImpl<>(content, pageable, total == null ? 0L : total); } - private BooleanBuilder buildPredicate(AuctionHistorySearchRequest c, QAuctionHistory ah) { + /** 거래내역 기본 조건 빌드 (카테고리, 아이템명, 가격, 거래일자) */ + private BooleanBuilder buildHistoryPredicate( + AuctionHistorySearchRequest c, QAuctionHistory ah) { BooleanBuilder builder = new BooleanBuilder(); + + // 기본 조건들 if (c.itemTopCategory() != null && !c.itemTopCategory().isBlank()) { builder.and(ah.itemTopCategory.eq(c.itemTopCategory())); } @@ -48,6 +106,338 @@ private BooleanBuilder buildPredicate(AuctionHistorySearchRequest c, QAuctionHis if (c.itemName() != null && !c.itemName().isBlank()) { builder.and(ah.itemName.containsIgnoreCase(c.itemName())); } + + // 가격 조건 (PriceSearchRequest가 있으면) + if (c.priceSearchRequest() != null) { + PriceSearchRequest price = c.priceSearchRequest(); + if (price.priceFrom() != null) { + builder.and(ah.auctionPricePerUnit.goe(price.priceFrom())); + } + if (price.priceTo() != null) { + builder.and(ah.auctionPricePerUnit.loe(price.priceTo())); + } + } + + // 거래 일자 조건 (String 'yyyy-MM-dd' → Instant 변환) + if (c.dateAuctionBuyRequest() != null) { + DateAuctionBuyRequest date = c.dateAuctionBuyRequest(); + if (date.dateAuctionBuyFrom() != null && !date.dateAuctionBuyFrom().isBlank()) { + Instant fromInstant = + LocalDate.parse(date.dateAuctionBuyFrom()) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant(); + builder.and(ah.dateAuctionBuy.goe(fromInstant)); + } + if (date.dateAuctionBuyTo() != null && !date.dateAuctionBuyTo().isBlank()) { + Instant toInstant = + LocalDate.parse(date.dateAuctionBuyTo()) + .plusDays(1) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant(); + builder.and(ah.dateAuctionBuy.lt(toInstant)); + } + } + return builder; } + + /** 옵션 검색 조건 빌드 (서브쿼리용) */ + private OptionConditionResult buildItemOptionConditions( + ItemOptionSearchRequest opt, QAuctionItemOption aio) { + BooleanBuilder builder = new BooleanBuilder(); + int conditionCount = 0; + boolean ergConditionAdded = false; // 에르그 조건 추가 여부 (레벨/랭크 통합) + + // 1. Balance (밸런스) + if (opt.balanceSearch() != null && opt.balanceSearch().balance() != null) { + builder.or( + buildOptionCondition( + aio, + "밸런스", + opt.balanceSearch().balance(), + opt.balanceSearch().balanceStandard())); + conditionCount++; + } + + // 2. Critical (크리티컬) + if (opt.criticalSearch() != null && opt.criticalSearch().critical() != null) { + builder.or( + buildOptionCondition( + aio, + "크리티컬", + opt.criticalSearch().critical(), + opt.criticalSearch().criticalStandard())); + conditionCount++; + } + + // 3. Defense (방어력) + if (opt.defenseSearch() != null && opt.defenseSearch().defense() != null) { + builder.or( + buildOptionCondition( + aio, + "방어력", + opt.defenseSearch().defense(), + opt.defenseSearch().defenseStandard())); + conditionCount++; + } + + // 4. Erg (에르그) - 범위 검색 + if (opt.ergSearch() != null) { + BooleanExpression ergTypeCondition = aio.optionType.eq("에르그"); + BooleanExpression ergValueCondition = null; + + if (opt.ergSearch().ergFrom() != null && opt.ergSearch().ergTo() != null) { + ergValueCondition = + castOptionValueToInt(aio) + .between(opt.ergSearch().ergFrom(), opt.ergSearch().ergTo()); + } else if (opt.ergSearch().ergFrom() != null) { + ergValueCondition = castOptionValueToInt(aio).goe(opt.ergSearch().ergFrom()); + } else if (opt.ergSearch().ergTo() != null) { + ergValueCondition = castOptionValueToInt(aio).loe(opt.ergSearch().ergTo()); + } + + if (ergValueCondition != null) { + // 명시적으로 괄호를 추가 + BooleanExpression combined = ergTypeCondition.and(ergValueCondition); + builder.or(Expressions.booleanTemplate("({0})", combined)); + // 에르그는 레벨/랭크 통합하여 1개로 카운트 + if (!ergConditionAdded) { + conditionCount++; + ergConditionAdded = true; + } + } + } + + // 5. ErgRank (에르그 등급) - 문자열 비교 + if (opt.ergRankSearch() != null && opt.ergRankSearch().ergRank() != null) { + BooleanExpression combined = + aio.optionType.eq("에르그").and(aio.optionValue.eq(opt.ergRankSearch().ergRank())); + builder.or(Expressions.booleanTemplate("({0})", combined)); + // 에르그는 레벨/랭크 통합하여 1개로 카운트 + if (!ergConditionAdded) { + conditionCount++; + ergConditionAdded = true; + } + } + + // 6. MagicDefense (마법 방어력) + if (opt.magicDefenseSearch() != null && opt.magicDefenseSearch().magicDefense() != null) { + builder.or( + buildOptionCondition( + aio, + "마법 방어력", + opt.magicDefenseSearch().magicDefense(), + opt.magicDefenseSearch().magicDefenseStandard())); + conditionCount++; + } + + // 7. MagicProtect (마법 보호) + if (opt.magicProtectSearch() != null && opt.magicProtectSearch().magicProtect() != null) { + builder.or( + buildOptionCondition( + aio, + "마법 보호", + opt.magicProtectSearch().magicProtect(), + opt.magicProtectSearch().magicProtectStandard())); + conditionCount++; + } + + // 8. MaxAttack (공격) - 범위 검색 + if (opt.maxAttackSearch() != null) { + BooleanExpression attackTypeCondition = aio.optionType.eq("공격"); + BooleanExpression attackValueCondition = null; + + if (opt.maxAttackSearch().maxAttackFrom() != null + && opt.maxAttackSearch().maxAttackTo() != null) { + attackValueCondition = + castOptionValueToInt(aio) + .between( + opt.maxAttackSearch().maxAttackFrom(), + opt.maxAttackSearch().maxAttackTo()); + } else if (opt.maxAttackSearch().maxAttackFrom() != null) { + attackValueCondition = + castOptionValueToInt(aio).goe(opt.maxAttackSearch().maxAttackFrom()); + } else if (opt.maxAttackSearch().maxAttackTo() != null) { + attackValueCondition = + castOptionValueToInt(aio).loe(opt.maxAttackSearch().maxAttackTo()); + } + + if (attackValueCondition != null) { + // 명시적으로 괄호를 추가 + BooleanExpression combined = attackTypeCondition.and(attackValueCondition); + builder.or(Expressions.booleanTemplate("({0})", combined)); + conditionCount++; + } + } + + // 9. MaximumDurability (내구력) + if (opt.maximumDurabilitySearch() != null + && opt.maximumDurabilitySearch().maximumDurability() != null) { + builder.or( + buildOptionCondition( + aio, + "내구력", + opt.maximumDurabilitySearch().maximumDurability(), + opt.maximumDurabilitySearch().maximumDurabilityStandard())); + conditionCount++; + } + + // 10. MaxInjuryRate (부상률) - 범위 검색 + if (opt.maxInjuryRateSearch() != null) { + BooleanExpression injuryTypeCondition = aio.optionType.eq("부상률"); + BooleanExpression injuryValueCondition = null; + + if (opt.maxInjuryRateSearch().maxInjuryRateFrom() != null + && opt.maxInjuryRateSearch().maxInjuryRateTo() != null) { + injuryValueCondition = + castOptionValueToInt(aio) + .between( + opt.maxInjuryRateSearch().maxInjuryRateFrom(), + opt.maxInjuryRateSearch().maxInjuryRateTo()); + } else if (opt.maxInjuryRateSearch().maxInjuryRateFrom() != null) { + injuryValueCondition = + castOptionValueToInt(aio) + .goe(opt.maxInjuryRateSearch().maxInjuryRateFrom()); + } else if (opt.maxInjuryRateSearch().maxInjuryRateTo() != null) { + injuryValueCondition = + castOptionValueToInt(aio).loe(opt.maxInjuryRateSearch().maxInjuryRateTo()); + } + + if (injuryValueCondition != null) { + // 명시적으로 괄호를 추가 + BooleanExpression combined = injuryTypeCondition.and(injuryValueCondition); + builder.or(Expressions.booleanTemplate("({0})", combined)); + conditionCount++; + } + } + + // 11. Proficiency (숙련) + if (opt.proficiencySearch() != null && opt.proficiencySearch().proficiency() != null) { + builder.or( + buildOptionCondition( + aio, + "숙련", + opt.proficiencySearch().proficiency(), + opt.proficiencySearch().proficiencyStandard())); + conditionCount++; + } + + // 12. Protect (보호) + if (opt.protectSearch() != null && opt.protectSearch().protect() != null) { + builder.or( + buildOptionCondition( + aio, + "보호", + opt.protectSearch().protect(), + opt.protectSearch().protectStandard())); + conditionCount++; + } + + // 13. RemainingTransactionCount (남은 거래 횟수) + if (opt.remainingTransactionCountSearch() != null + && opt.remainingTransactionCountSearch().remainingTransactionCount() != null) { + builder.or( + buildOptionCondition( + aio, + "남은 거래 횟수", + opt.remainingTransactionCountSearch().remainingTransactionCount(), + opt.remainingTransactionCountSearch() + .remainingTransactionCountStandard())); + conditionCount++; + } + + // 14. RemainingUnsealCount (남은 전용 해제 가능 횟수) + if (opt.remainingUnsealCountSearch() != null + && opt.remainingUnsealCountSearch().remainingUnsealCount() != null) { + builder.or( + buildOptionCondition( + aio, + "남은 전용 해제 가능 횟수", + opt.remainingUnsealCountSearch().remainingUnsealCount(), + opt.remainingUnsealCountSearch().remainingUnsealCountStandard())); + conditionCount++; + } + + // 15. RemainingUseCount (남은 사용 횟수) + if (opt.remainingUseCountSearch() != null + && opt.remainingUseCountSearch().remainingUseCount() != null) { + builder.or( + buildOptionCondition( + aio, + "남은 사용 횟수", + opt.remainingUseCountSearch().remainingUseCount(), + opt.remainingUseCountSearch().remainingUseCountStandard())); + conditionCount++; + } + + // 16. WearingRestrictions (착용 제한) - 문자열 비교 + if (opt.wearingRestrictionsSearch() != null + && opt.wearingRestrictionsSearch().wearingRestrictions() != null) { + BooleanExpression condition = + aio.optionValue.contains(opt.wearingRestrictionsSearch().wearingRestrictions()); + builder.or(Expressions.booleanTemplate("({0})", condition)); + conditionCount++; + } + + return new OptionConditionResult(builder, conditionCount); + } + + /** 옵션 조건 빌드 헬퍼 (option_type + 숫자 비교 + SearchStandard) */ + private BooleanExpression buildOptionCondition( + QAuctionItemOption aio, String optionType, Integer value, SearchStandard standard) { + BooleanExpression optionTypeCondition = aio.optionType.eq(optionType); + + NumberTemplate numValue = castOptionValueToInt(aio); + + BooleanExpression valueCondition; + if (standard == null || standard.isEqual()) { + valueCondition = numValue.eq(value); // 같음 + } else if (standard.isUp()) { + valueCondition = numValue.goe(value); // 이상 (>=) + } else if (standard.isDown()) { + valueCondition = numValue.loe(value); // 이하 (<=) + } else { + valueCondition = numValue.eq(value); // 기본값: 같음 + } + + // 명시적 괄호 추가 + BooleanExpression combined = optionTypeCondition.and(valueCondition); + return Expressions.booleanTemplate("({0})", combined); + } + + /** option_value2 또는 option_value를 Integer로 변환하는 NumberTemplate */ + private NumberTemplate castOptionValueToInt(QAuctionItemOption aio) { + return Expressions.numberTemplate( + Integer.class, "COALESCE({0}, {1}, 0)", aio.optionValue2, aio.optionValue); + } + + /** Pageable의 Sort를 QueryDSL OrderSpecifier로 변환 */ + private List> buildOrderSpecifiers(Pageable pageable, QAuctionHistory ah) { + List> orders = new ArrayList<>(); + + if (pageable.getSort().isSorted()) { + for (Sort.Order order : pageable.getSort()) { + Order direction = order.isAscending() ? Order.ASC : Order.DESC; + String property = order.getProperty(); + + // 필드명에 따라 QueryDSL 정렬 표현식 생성 + OrderSpecifier orderSpecifier = + switch (property) { + case "dateAuctionBuy" -> + new OrderSpecifier<>(direction, ah.dateAuctionBuy); + case "auctionPricePerUnit" -> + new OrderSpecifier<>(direction, ah.auctionPricePerUnit); + case "itemName" -> new OrderSpecifier<>(direction, ah.itemName); + default -> new OrderSpecifier<>(Order.DESC, ah.dateAuctionBuy); // 기본값 + }; + + orders.add(orderSpecifier); + } + } else { + // 정렬 조건이 없으면 기본 정렬은 최신 거래일자순: dateAuctionBuy DESC + orders.add(new OrderSpecifier<>(Order.DESC, ah.dateAuctionBuy)); + } + + return orders; + } } diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/controller/AuctionHistoryController.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/controller/AuctionHistoryController.java index d51f0b0..613622f 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/controller/AuctionHistoryController.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/controller/AuctionHistoryController.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import until.the.eternity.auctionhistory.application.scheduler.AuctionHistoryScheduler; @@ -26,8 +27,8 @@ public class AuctionHistoryController { @GetMapping("/search") @Operation(summary = "경매장 거래 내역 검색", description = "Nexon Open API 경매장 거래 내역 검색") public ResponseEntity>> search( - @ModelAttribute PageRequestDto pageDto, - @ModelAttribute @Valid AuctionHistorySearchRequest requestDto) { + @ParameterObject @ModelAttribute PageRequestDto pageDto, + @ParameterObject @ModelAttribute @Valid AuctionHistorySearchRequest requestDto) { PageResponseDto> result = service.search(requestDto, pageDto); return ResponseEntity.ok(result); diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SearchStandard.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SearchStandard.java new file mode 100644 index 0000000..ef00567 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SearchStandard.java @@ -0,0 +1,54 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Arrays; + +/** 검색 기준 (이상/이하/같음) */ +@Schema(description = "검색 기준", enumAsRef = true) +public enum SearchStandard { + UP("UP", "이상"), + DOWN("DOWN", "이하"), + EQUAL("EQUAL", "같음"); + + private final String code; + private final String description; + + SearchStandard(String code, String description) { + this.code = code; + this.description = description; + } + + @JsonValue + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + @JsonCreator + public static SearchStandard from(String code) { + return Arrays.stream(SearchStandard.values()) + .filter(standard -> standard.code.equalsIgnoreCase(code)) + .findFirst() + .orElse(null); + } + + /** UP 조건인지 확인 */ + public boolean isUp() { + return this == UP; + } + + /** DOWN 조건인지 확인 */ + public boolean isDown() { + return this == DOWN; + } + + /** EQUAL 조건인지 확인 */ + public boolean isEqual() { + return this == EQUAL; + } +} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SortDirection.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SortDirection.java new file mode 100644 index 0000000..8266606 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/enums/SortDirection.java @@ -0,0 +1,48 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Arrays; + +/** 정렬 방향 (오름차순/내림차순) */ +@Schema(description = "정렬 방향", enumAsRef = true) +public enum SortDirection { + ASC("ASC", "오름차순"), + DESC("DESC", "내림차순"); + + private final String code; + private final String description; + + SortDirection(String code, String description) { + this.code = code; + this.description = description; + } + + @JsonValue + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + @JsonCreator + public static SortDirection from(String code) { + return Arrays.stream(SortDirection.values()) + .filter(direction -> direction.code.equalsIgnoreCase(code)) + .findFirst() + .orElse(DESC); // 기본값: 내림차순 + } + + /** 오름차순인지 확인 */ + public boolean isAscending() { + return this == ASC; + } + + /** 내림차순인지 확인 */ + public boolean isDescending() { + return this == DESC; + } +} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/AuctionHistorySearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/AuctionHistorySearchRequest.java index 4d96f84..f68928f 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/AuctionHistorySearchRequest.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/AuctionHistorySearchRequest.java @@ -3,10 +3,11 @@ import io.swagger.v3.oas.annotations.media.Schema; /** 경매 히스토리 검색 조건 DTO - 페이지네이션 포함 */ +@Schema(description = "경매 거래내역 검색 조건") public record AuctionHistorySearchRequest( @Schema(description = "아이템 이름 (like 검색)", example = "페러시우스 타이탄 블레이드") String itemName, @Schema(description = "대분류 카테고리", example = "근거리 장비") String itemTopCategory, @Schema(description = "소분류 카테고리", example = "검") String itemSubCategory, - // TODO: 거래 가격, 거래 일자를 범위 검색으로 변경, 옵션은 별도의 RequestDTO 구현 - @Schema(description = "거래 가격", example = "10000000") String auction_price_per_unit, - @Schema(description = "거래 일자", example = "검") String date_auction_buy) {} + @Schema(description = "거래 일자 조건") DateAuctionBuyRequest dateAuctionBuyRequest, + @Schema(description = "가격 검색 조건") PriceSearchRequest priceSearchRequest, + @Schema(description = "아이템 옵션 검색 조건") ItemOptionSearchRequest itemOptionSearchRequest) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/BalanceSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/BalanceSearchRequest.java new file mode 100644 index 0000000..17b96a6 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/BalanceSearchRequest.java @@ -0,0 +1,10 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import until.the.eternity.auctionhistory.interfaces.rest.dto.enums.SearchStandard; + +@Schema(description = "밸런스 검색 조건") +public record BalanceSearchRequest( + @Schema(description = "밸런스 값", example = "10") Integer balance, + @Schema(description = "검색 기준 (UP: 이상, DOWN: 이하, EQUAL: 같음)", example = "UP") + SearchStandard balanceStandard) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/CriticalSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/CriticalSearchRequest.java new file mode 100644 index 0000000..9ce0400 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/CriticalSearchRequest.java @@ -0,0 +1,10 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import until.the.eternity.auctionhistory.interfaces.rest.dto.enums.SearchStandard; + +@Schema(description = "크리티컬 검색 조건") +public record CriticalSearchRequest( + @Schema(description = "크리티컬 값", example = "30") Integer critical, + @Schema(description = "검색 기준 (UP: 이상, DOWN: 이하, EQUAL: 같음)", example = "UP") + SearchStandard criticalStandard) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/DateAuctionBuyRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/DateAuctionBuyRequest.java new file mode 100644 index 0000000..715a807 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/DateAuctionBuyRequest.java @@ -0,0 +1,8 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "거래 일자 조건") +public record DateAuctionBuyRequest( + @Schema(description = "거래 일자 시작 범위", example = "2025-01-01") String dateAuctionBuyFrom, + @Schema(description = "거래 일자 종료 범위", example = "2025-12-31") String dateAuctionBuyTo) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/DefenseSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/DefenseSearchRequest.java new file mode 100644 index 0000000..ff23cbb --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/DefenseSearchRequest.java @@ -0,0 +1,10 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import until.the.eternity.auctionhistory.interfaces.rest.dto.enums.SearchStandard; + +@Schema(description = "방어력 검색 조건") +public record DefenseSearchRequest( + @Schema(description = "방어력 값", example = "5") Integer defense, + @Schema(description = "검색 기준 (UP: 이상, DOWN: 이하, EQUAL: 같음)", example = "DOWN") + SearchStandard defenseStandard) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ErgRankSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ErgRankSearchRequest.java new file mode 100644 index 0000000..e892480 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ErgRankSearchRequest.java @@ -0,0 +1,11 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "에르그 등급 검색 조건") +public record ErgRankSearchRequest( + @Schema( + description = "에르그 등급", + example = "S등급", + allowableValues = {"S등급", "A등급", "B등급"}) + String ergRank) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ErgSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ErgSearchRequest.java new file mode 100644 index 0000000..930869c --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ErgSearchRequest.java @@ -0,0 +1,8 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "에르그 검색 조건 (범위)") +public record ErgSearchRequest( + @Schema(description = "에르그 최소값", example = "10") Integer ergFrom, + @Schema(description = "에르그 최대값", example = "50") Integer ergTo) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ItemOptionSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ItemOptionSearchRequest.java new file mode 100644 index 0000000..742916f --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ItemOptionSearchRequest.java @@ -0,0 +1,27 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "아이템 옵션 검색 조건 통합") +public record ItemOptionSearchRequest( + @Schema(description = "밸런스 검색 조건") BalanceSearchRequest balanceSearch, + @Schema(description = "크리티컬 검색 조건") CriticalSearchRequest criticalSearch, + @Schema(description = "방어력 검색 조건") DefenseSearchRequest defenseSearch, + @Schema(description = "에르그 검색 조건") ErgSearchRequest ergSearch, + @Schema(description = "에르그 등급 검색 조건") ErgRankSearchRequest ergRankSearch, + @Schema(description = "마법 방어력 검색 조건") MagicDefenseSearchRequest magicDefenseSearch, + @Schema(description = "마법 보호 검색 조건") MagicProtectSearchRequest magicProtectSearch, + @Schema(description = "최대 공격력 검색 조건") MaxAttackSearchRequest maxAttackSearch, + @Schema(description = "최대 내구력 검색 조건") + MaximumDurabilitySearchRequest maximumDurabilitySearch, + @Schema(description = "최대 부상률 검색 조건") MaxInjuryRateSearchRequest maxInjuryRateSearch, + @Schema(description = "숙련도 검색 조건") ProficiencySearchRequest proficiencySearch, + @Schema(description = "보호 검색 조건") ProtectSearchRequest protectSearch, + @Schema(description = "남은 거래 횟수 검색 조건") + RemainingTransactionCountSearchRequest remainingTransactionCountSearch, + @Schema(description = "남은 전용 해제 가능 횟수 검색 조건") + RemainingUnsealCountSearchRequest remainingUnsealCountSearch, + @Schema(description = "남은 사용 횟수 검색 조건") + RemainingUseCountSearchRequest remainingUseCountSearch, + @Schema(description = "착용 제한 검색 조건") + WearingRestrictionsSearchRequest wearingRestrictionsSearch) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MagicDefenseSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MagicDefenseSearchRequest.java new file mode 100644 index 0000000..24f6c5d --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MagicDefenseSearchRequest.java @@ -0,0 +1,10 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import until.the.eternity.auctionhistory.interfaces.rest.dto.enums.SearchStandard; + +@Schema(description = "마법 방어력 검색 조건") +public record MagicDefenseSearchRequest( + @Schema(description = "마법 방어력 값", example = "3") Integer magicDefense, + @Schema(description = "검색 기준 (UP: 이상, DOWN: 이하, EQUAL: 같음)", example = "UP") + SearchStandard magicDefenseStandard) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MagicProtectSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MagicProtectSearchRequest.java new file mode 100644 index 0000000..db85322 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MagicProtectSearchRequest.java @@ -0,0 +1,10 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import until.the.eternity.auctionhistory.interfaces.rest.dto.enums.SearchStandard; + +@Schema(description = "마법 보호 검색 조건") +public record MagicProtectSearchRequest( + @Schema(description = "마법 보호 값", example = "2") Integer magicProtect, + @Schema(description = "검색 기준 (UP: 이상, DOWN: 이하, EQUAL: 같음)", example = "UP") + SearchStandard magicProtectStandard) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MaxAttackSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MaxAttackSearchRequest.java new file mode 100644 index 0000000..d7969d2 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MaxAttackSearchRequest.java @@ -0,0 +1,8 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "최대 공격력 검색 조건 (범위)") +public record MaxAttackSearchRequest( + @Schema(description = "최대 공격력 최소값", example = "50") Integer maxAttackFrom, + @Schema(description = "최대 공격력 최대값", example = "100") Integer maxAttackTo) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MaxInjuryRateSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MaxInjuryRateSearchRequest.java new file mode 100644 index 0000000..94daf0d --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MaxInjuryRateSearchRequest.java @@ -0,0 +1,8 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "최대 부상률 검색 조건 (범위)") +public record MaxInjuryRateSearchRequest( + @Schema(description = "최대 부상률 최소값", example = "10") Integer maxInjuryRateFrom, + @Schema(description = "최대 부상률 최대값", example = "30") Integer maxInjuryRateTo) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MaximumDurabilitySearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MaximumDurabilitySearchRequest.java new file mode 100644 index 0000000..1ab5c1a --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/MaximumDurabilitySearchRequest.java @@ -0,0 +1,10 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import until.the.eternity.auctionhistory.interfaces.rest.dto.enums.SearchStandard; + +@Schema(description = "최대 내구력 검색 조건") +public record MaximumDurabilitySearchRequest( + @Schema(description = "최대 내구력 값", example = "20") Integer maximumDurability, + @Schema(description = "검색 기준 (UP: 이상, DOWN: 이하, EQUAL: 같음)", example = "UP") + SearchStandard maximumDurabilityStandard) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PriceSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PriceSearchRequest.java index 9c82c49..7cb007d 100644 --- a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PriceSearchRequest.java +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/PriceSearchRequest.java @@ -2,7 +2,7 @@ import io.swagger.v3.oas.annotations.media.Schema; +@Schema(description = "가격 검색 조건") public record PriceSearchRequest( - @Schema(description = "가격 최소값", example = "0", defaultValue = "0") long PriceTo, - @Schema(description = "가격 최대값", example = "9999999999", defaultValue = "9999999999") - long PriceFrom) {} + @Schema(description = "가격 최소값", example = "0") Long priceFrom, + @Schema(description = "가격 최대값", example = "9999999999") Long priceTo) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ProficiencySearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ProficiencySearchRequest.java new file mode 100644 index 0000000..78f16ab --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ProficiencySearchRequest.java @@ -0,0 +1,10 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import until.the.eternity.auctionhistory.interfaces.rest.dto.enums.SearchStandard; + +@Schema(description = "숙련도 검색 조건") +public record ProficiencySearchRequest( + @Schema(description = "숙련도 값", example = "15") Integer proficiency, + @Schema(description = "검색 기준 (UP: 이상, DOWN: 이하, EQUAL: 같음)", example = "UP") + SearchStandard proficiencyStandard) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ProtectSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ProtectSearchRequest.java new file mode 100644 index 0000000..f3bcd36 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/ProtectSearchRequest.java @@ -0,0 +1,10 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import until.the.eternity.auctionhistory.interfaces.rest.dto.enums.SearchStandard; + +@Schema(description = "보호 검색 조건") +public record ProtectSearchRequest( + @Schema(description = "보호 값", example = "1") Integer protect, + @Schema(description = "검색 기준 (UP: 이상, DOWN: 이하, EQUAL: 같음)", example = "DOWN") + SearchStandard protectStandard) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/RemainingTransactionCountSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/RemainingTransactionCountSearchRequest.java new file mode 100644 index 0000000..1539799 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/RemainingTransactionCountSearchRequest.java @@ -0,0 +1,10 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import until.the.eternity.auctionhistory.interfaces.rest.dto.enums.SearchStandard; + +@Schema(description = "남은 거래 횟수 검색 조건") +public record RemainingTransactionCountSearchRequest( + @Schema(description = "남은 거래 횟수", example = "5") Integer remainingTransactionCount, + @Schema(description = "검색 기준 (UP: 이상, DOWN: 이하, EQUAL: 같음)", example = "UP") + SearchStandard remainingTransactionCountStandard) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/RemainingUnsealCountSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/RemainingUnsealCountSearchRequest.java new file mode 100644 index 0000000..d2ece9e --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/RemainingUnsealCountSearchRequest.java @@ -0,0 +1,10 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import until.the.eternity.auctionhistory.interfaces.rest.dto.enums.SearchStandard; + +@Schema(description = "남은 전용 해제 가능 횟수 검색 조건") +public record RemainingUnsealCountSearchRequest( + @Schema(description = "남은 전용 해제 가능 횟수", example = "3") Integer remainingUnsealCount, + @Schema(description = "검색 기준 (UP: 이상, DOWN: 이하, EQUAL: 같음)", example = "UP") + SearchStandard remainingUnsealCountStandard) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/RemainingUseCountSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/RemainingUseCountSearchRequest.java new file mode 100644 index 0000000..c4fd7dd --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/RemainingUseCountSearchRequest.java @@ -0,0 +1,10 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import until.the.eternity.auctionhistory.interfaces.rest.dto.enums.SearchStandard; + +@Schema(description = "남은 사용 횟수 검색 조건") +public record RemainingUseCountSearchRequest( + @Schema(description = "남은 사용 횟수", example = "10") Integer remainingUseCount, + @Schema(description = "검색 기준 (UP: 이상, DOWN: 이하, EQUAL: 같음)", example = "DOWN") + SearchStandard remainingUseCountStandard) {} diff --git a/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/WearingRestrictionsSearchRequest.java b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/WearingRestrictionsSearchRequest.java new file mode 100644 index 0000000..d1a3a30 --- /dev/null +++ b/src/main/java/until/the/eternity/auctionhistory/interfaces/rest/dto/request/WearingRestrictionsSearchRequest.java @@ -0,0 +1,7 @@ +package until.the.eternity.auctionhistory.interfaces.rest.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; + +@Schema(description = "착용 제한 검색 조건") +public record WearingRestrictionsSearchRequest( + @Schema(description = "착용 제한", example = "자이언트 전용") String wearingRestrictions) {} diff --git a/src/main/java/until/the/eternity/common/enums/SortDirection.java b/src/main/java/until/the/eternity/common/enums/SortDirection.java new file mode 100644 index 0000000..5e688da --- /dev/null +++ b/src/main/java/until/the/eternity/common/enums/SortDirection.java @@ -0,0 +1,57 @@ +package until.the.eternity.common.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Arrays; +import org.springframework.data.domain.Sort; + +/** 정렬 방향 (오름차순/내림차순) */ +@Schema(description = "정렬 방향", enumAsRef = true) +public enum SortDirection { + ASC("ASC", "오름차순"), + DESC("DESC", "내림차순"); + + private final String code; + private final String description; + + SortDirection(String code, String description) { + this.code = code; + this.description = description; + } + + @JsonValue + public String getCode() { + return code; + } + + public String getDescription() { + return description; + } + + @JsonCreator + public static SortDirection from(String code) { + if (code == null) { + return DESC; // 기본값: 내림차순 + } + return Arrays.stream(SortDirection.values()) + .filter(direction -> direction.code.equalsIgnoreCase(code)) + .findFirst() + .orElse(DESC); + } + + /** 오름차순인지 확인 */ + public boolean isAscending() { + return this == ASC; + } + + /** 내림차순인지 확인 */ + public boolean isDescending() { + return this == DESC; + } + + /** Spring Data Sort.Direction으로 변환 */ + public Sort.Direction toSpringDirection() { + return this == ASC ? Sort.Direction.ASC : Sort.Direction.DESC; + } +} diff --git a/src/main/java/until/the/eternity/common/enums/SortField.java b/src/main/java/until/the/eternity/common/enums/SortField.java new file mode 100644 index 0000000..704d79d --- /dev/null +++ b/src/main/java/until/the/eternity/common/enums/SortField.java @@ -0,0 +1,42 @@ +package until.the.eternity.common.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.Arrays; + +/** 정렬 필드 */ +@Schema(description = "정렬 필드", enumAsRef = true) +public enum SortField { + DATE_AUCTION_BUY("dateAuctionBuy", "거래 일자"), + AUCTION_PRICE_PER_UNIT("auctionPricePerUnit", "개당 가격"), + ITEM_NAME("itemName", "아이템 이름"); + + private final String fieldName; + private final String description; + + SortField(String fieldName, String description) { + this.fieldName = fieldName; + this.description = description; + } + + @JsonValue + public String getFieldName() { + return fieldName; + } + + public String getDescription() { + return description; + } + + @JsonCreator + public static SortField from(String fieldName) { + if (fieldName == null) { + return DATE_AUCTION_BUY; // 기본값: 거래 일자 + } + return Arrays.stream(SortField.values()) + .filter(field -> field.fieldName.equalsIgnoreCase(fieldName)) + .findFirst() + .orElse(DATE_AUCTION_BUY); + } +} diff --git a/src/main/java/until/the/eternity/common/request/PageRequestDto.java b/src/main/java/until/the/eternity/common/request/PageRequestDto.java index 95296ad..a2fbb81 100644 --- a/src/main/java/until/the/eternity/common/request/PageRequestDto.java +++ b/src/main/java/until/the/eternity/common/request/PageRequestDto.java @@ -6,31 +6,34 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import until.the.eternity.common.enums.SortDirection; +import until.the.eternity.common.enums.SortField; @Schema(description = "페이지 요청 파라미터") public record PageRequestDto( @Schema(description = "요청할 페이지 번호 (1부터 시작)", example = "1") @Min(1) Integer page, @Schema(description = "페이지당 항목 수", example = "20") @Min(1) @Max(100) Integer size, - @Schema(description = "정렬 필드 (예: createdAt)", example = "createdAt") String sortBy, - @Schema(description = "정렬 방향 (asc or desc)", example = "desc") String direction) { + @Schema( + description = "정렬 필드 (dateAuctionBuy, auctionPricePerUnit, itemName)", + example = "dateAuctionBuy") + SortField sortBy, + @Schema(description = "정렬 방향 (ASC, DESC)", example = "DESC") SortDirection direction) { private static final int DEFAULT_PAGE = 1; private static final int DEFAULT_SIZE = 20; - private static final String DEFAULT_SORT_BY = "id"; - private static final String DEFAULT_DIRECTION = "desc"; + private static final SortField DEFAULT_SORT_BY = SortField.DATE_AUCTION_BUY; + private static final SortDirection DEFAULT_DIRECTION = SortDirection.DESC; public Pageable toPageable() { int resolvedPage = this.page != null ? this.page - 1 : DEFAULT_PAGE; int resolvedSize = this.size != null ? this.size : DEFAULT_SIZE; - String resolvedSortBy = this.sortBy != null ? this.sortBy : DEFAULT_SORT_BY; - Sort.Direction resolvedDirection = parseDirection(this.direction); + SortField resolvedSortBy = this.sortBy != null ? this.sortBy : DEFAULT_SORT_BY; + SortDirection resolvedDirection = + this.direction != null ? this.direction : DEFAULT_DIRECTION; return PageRequest.of( - resolvedPage, resolvedSize, Sort.by(resolvedDirection, resolvedSortBy)); - } - - private Sort.Direction parseDirection(String dir) { - if ("asc".equalsIgnoreCase(dir)) return Sort.Direction.ASC; - return Sort.Direction.DESC; + resolvedPage, + resolvedSize, + Sort.by(resolvedDirection.toSpringDirection(), resolvedSortBy.getFieldName())); } } diff --git a/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java b/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java index 9e8db8e..d53cd3b 100644 --- a/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java +++ b/src/main/java/until/the/eternity/iteminfo/domain/entity/ItemInfoId.java @@ -3,11 +3,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; import java.io.Serializable; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; -import lombok.EqualsAndHashCode; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; @Embeddable @Getter diff --git a/src/test/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryServiceTest.java b/src/test/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryServiceTest.java index 8c438c2..ed2fea8 100644 --- a/src/test/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryServiceTest.java +++ b/src/test/java/until/the/eternity/auctionhistory/application/service/AuctionHistoryServiceTest.java @@ -42,7 +42,7 @@ class AuctionHistoryServiceTest { void search_should_return_paged_response() { // given AuctionHistorySearchRequest searchRequest = - new AuctionHistorySearchRequest(null, null, null, null, null); + new AuctionHistorySearchRequest(null, null, null, null, null, null); PageRequestDto pageRequestDto = mock(PageRequestDto.class); Pageable pageable = PageRequest.of(0, 10); when(pageRequestDto.toPageable()).thenReturn(pageable); diff --git a/src/test/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoServiceTest.java b/src/test/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoServiceTest.java index 45f7b8d..7de499a 100644 --- a/src/test/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoServiceTest.java +++ b/src/test/java/until/the/eternity/metalwareinfo/application/service/MetalwareInfoServiceTest.java @@ -1,7 +1,8 @@ package until.the.eternity.metalwareinfo.application.service; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.util.List; import org.junit.jupiter.api.DisplayName;