diff --git a/.github/workflows/fe-cd-storybook.yml b/.github/workflows/fe-cd-storybook.yml index 6f3b4d846..654c65c66 100644 --- a/.github/workflows/fe-cd-storybook.yml +++ b/.github/workflows/fe-cd-storybook.yml @@ -46,7 +46,7 @@ jobs: API_BASE_URL: ${{ secrets.API_BASE_URL }} - name: build storybook - run: npm run build-storybook + run: npm run build:storybook - name: upload storybook uses: peaceiris/actions-gh-pages@v3 diff --git a/.github/workflows/fe-ci-dev.yml b/.github/workflows/fe-ci-dev.yml index bd4944970..9dc2c5f90 100644 --- a/.github/workflows/fe-ci-dev.yml +++ b/.github/workflows/fe-ci-dev.yml @@ -39,7 +39,7 @@ jobs: ${{ runner.os }}-node- - name: Build - run: npm run build-dev + run: npm run build:dev working-directory: ./frontend - name: Test diff --git a/backend/src/main/java/ddangkong/aop/logging/RequestLoggingAspect.java b/backend/src/main/java/ddangkong/aop/logging/ControllerLoggingAspect.java similarity index 84% rename from backend/src/main/java/ddangkong/aop/logging/RequestLoggingAspect.java rename to backend/src/main/java/ddangkong/aop/logging/ControllerLoggingAspect.java index 27622ae63..893e5e51b 100644 --- a/backend/src/main/java/ddangkong/aop/logging/RequestLoggingAspect.java +++ b/backend/src/main/java/ddangkong/aop/logging/ControllerLoggingAspect.java @@ -13,7 +13,7 @@ import org.springframework.web.context.request.ServletRequestAttributes; @Slf4j -abstract class RequestLoggingAspect { +abstract class ControllerLoggingAspect { @Pointcut("execution(* ddangkong.controller..*Controller.*(..))") public void allController() { @@ -27,11 +27,7 @@ public void polling() { public void allControllerWithoutPolling() { } - protected void logController(JoinPoint joinPoint) { - logRequest(joinPoint); - } - - private void logRequest(JoinPoint joinPoint) { + protected void logControllerRequest(JoinPoint joinPoint) { HttpServletRequest request = getHttpServletRequest(); String uri = request.getRequestURI(); String httpMethod = request.getMethod(); @@ -41,6 +37,14 @@ private void logRequest(JoinPoint joinPoint) { log.info("Request Logging: {} {} body - {} parameters - {}", httpMethod, uri, body, queryParameters); } + protected void logControllerResponse(JoinPoint joinPoint, Object responseBody) { + HttpServletRequest request = getHttpServletRequest(); + String uri = request.getRequestURI(); + String httpMethod = request.getMethod(); + + log.info("Response Logging: SUCCESS {} {} Body: {}", httpMethod, uri, responseBody); + } + private HttpServletRequest getHttpServletRequest() { ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); return requestAttributes.getRequest(); diff --git a/backend/src/main/java/ddangkong/aop/logging/DevControllerLoggingAspect.java b/backend/src/main/java/ddangkong/aop/logging/DevControllerLoggingAspect.java new file mode 100644 index 000000000..40d68829a --- /dev/null +++ b/backend/src/main/java/ddangkong/aop/logging/DevControllerLoggingAspect.java @@ -0,0 +1,25 @@ +package ddangkong.aop.logging; + + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Aspect +@Component +@Profile({"dev", "local"}) +public class DevControllerLoggingAspect extends ControllerLoggingAspect { + + @Before("allController()") + public void logControllerRequest(JoinPoint joinPoint) { + super.logControllerRequest(joinPoint); + } + + @AfterReturning(pointcut = "allController()", returning = "responseBody") + protected void logControllerResponse(JoinPoint joinPoint, Object responseBody) { + super.logControllerResponse(joinPoint, responseBody); + } +} diff --git a/backend/src/main/java/ddangkong/aop/logging/DevRequestLoggingAspect.java b/backend/src/main/java/ddangkong/aop/logging/DevRequestLoggingAspect.java deleted file mode 100644 index 0572cde47..000000000 --- a/backend/src/main/java/ddangkong/aop/logging/DevRequestLoggingAspect.java +++ /dev/null @@ -1,18 +0,0 @@ -package ddangkong.aop.logging; - - -import org.aspectj.lang.JoinPoint; -import org.aspectj.lang.annotation.Aspect; -import org.aspectj.lang.annotation.Before; -import org.springframework.context.annotation.Profile; -import org.springframework.stereotype.Component; - -@Aspect -@Component -@Profile({"dev", "local"}) -public class DevRequestLoggingAspect extends RequestLoggingAspect { - @Before("allController()") - public void logController(JoinPoint joinPoint) { - super.logController(joinPoint); - } -} diff --git a/backend/src/main/java/ddangkong/aop/logging/ProdRequestLoggingAspect.java b/backend/src/main/java/ddangkong/aop/logging/ProdControllerLoggingAspect.java similarity index 65% rename from backend/src/main/java/ddangkong/aop/logging/ProdRequestLoggingAspect.java rename to backend/src/main/java/ddangkong/aop/logging/ProdControllerLoggingAspect.java index ab8f0ddb8..51429c6e2 100644 --- a/backend/src/main/java/ddangkong/aop/logging/ProdRequestLoggingAspect.java +++ b/backend/src/main/java/ddangkong/aop/logging/ProdControllerLoggingAspect.java @@ -10,9 +10,10 @@ @Aspect @Component @Profile("prod") -public class ProdRequestLoggingAspect extends RequestLoggingAspect { +public class ProdControllerLoggingAspect extends ControllerLoggingAspect { + @Before("allControllerWithoutPolling()") - public void logController(JoinPoint joinPoint) { - super.logController(joinPoint); + public void logControllerRequest(JoinPoint joinPoint) { + super.logControllerRequest(joinPoint); } } diff --git a/backend/src/main/java/ddangkong/config/sql/DataSourceConfig.java b/backend/src/main/java/ddangkong/config/sql/DataSourceConfig.java new file mode 100644 index 000000000..9c4a29d78 --- /dev/null +++ b/backend/src/main/java/ddangkong/config/sql/DataSourceConfig.java @@ -0,0 +1,54 @@ +package ddangkong.config.sql; + +import ddangkong.config.sql.type.DataSourceType; +import java.util.HashMap; +import java.util.Map; +import javax.sql.DataSource; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy; + +@Profile("prod") +@Configuration +public class DataSourceConfig { + + @Bean + @ConfigurationProperties(prefix = "spring.datasource.source") + public DataSource sourceDataSource() { + return DataSourceBuilder.create() + .build(); + } + + @Bean + @ConfigurationProperties(prefix = "spring.datasource.replica") + public DataSource replicaDataSource() { + return DataSourceBuilder.create() + .build(); + } + + @Bean + public DataSource routingDataSource( + DataSource sourceDataSource, + DataSource replicaDataSource + ) { + Map dataSources = new HashMap<>(); + dataSources.put(DataSourceType.SOURCE, sourceDataSource); + dataSources.put(DataSourceType.REPLICA, replicaDataSource); + + RoutingDataSource routingDataSource = new RoutingDataSource(); + routingDataSource.setDefaultTargetDataSource(dataSources.get(DataSourceType.SOURCE)); + routingDataSource.setTargetDataSources(dataSources); + + return routingDataSource; + } + + @Primary + @Bean + public DataSource dataSource() { + return new LazyConnectionDataSourceProxy(routingDataSource(sourceDataSource(), replicaDataSource())); + } +} diff --git a/backend/src/main/java/ddangkong/config/sql/RoutingDataSource.java b/backend/src/main/java/ddangkong/config/sql/RoutingDataSource.java new file mode 100644 index 000000000..8a73602c0 --- /dev/null +++ b/backend/src/main/java/ddangkong/config/sql/RoutingDataSource.java @@ -0,0 +1,18 @@ +package ddangkong.config.sql; + +import ddangkong.config.sql.type.DataSourceType; +import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +public class RoutingDataSource extends AbstractRoutingDataSource { + + @Override + protected Object determineCurrentLookupKey() { + boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); + if (isReadOnly) { + return DataSourceType.REPLICA; + } else { + return DataSourceType.SOURCE; + } + } +} diff --git a/backend/src/main/java/ddangkong/config/sql/type/DataSourceType.java b/backend/src/main/java/ddangkong/config/sql/type/DataSourceType.java new file mode 100644 index 000000000..c46a4a559 --- /dev/null +++ b/backend/src/main/java/ddangkong/config/sql/type/DataSourceType.java @@ -0,0 +1,7 @@ +package ddangkong.config.sql.type; + +public enum DataSourceType { + SOURCE, + REPLICA, + ; +} diff --git a/backend/src/main/java/ddangkong/domain/room/balance/roomcontent/RoomContent.java b/backend/src/main/java/ddangkong/domain/room/balance/roomcontent/RoomContent.java index 9b7dacff6..b10287f90 100644 --- a/backend/src/main/java/ddangkong/domain/room/balance/roomcontent/RoomContent.java +++ b/backend/src/main/java/ddangkong/domain/room/balance/roomcontent/RoomContent.java @@ -24,6 +24,9 @@ public class RoomContent { private static final int DELAY_MSEC = 2_000; // TODO SEC로 변경 + private static final int GAME_START_WAITING_MSEC = 3_000; // TODO SEC로 변경 + private static final int FIRST_ROUND = 1; + private static final int MSEC = 1_000; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @@ -58,7 +61,11 @@ public void updateVoteDeadline(LocalDateTime now, int timeLimit) { throw new VoteDeadlineConfiguredException(); } - int afterSec = (timeLimit + DELAY_MSEC) / 1_000; + int afterSec = (timeLimit + DELAY_MSEC) / MSEC; + if (round == FIRST_ROUND) { + afterSec += GAME_START_WAITING_MSEC / MSEC; + } + voteDeadline = now.plusSeconds(afterSec); } diff --git a/backend/src/main/resources/application-prod.yml b/backend/src/main/resources/application-prod.yml index 61b2885b7..05efe6406 100644 --- a/backend/src/main/resources/application-prod.yml +++ b/backend/src/main/resources/application-prod.yml @@ -7,11 +7,11 @@ spring: username: ${secret.datasource.source.username} password: ${secret.datasource.source.password} jdbc-url: jdbc:mysql://${secret.datasource.source.host}:${secret.datasource.source.port}/${secret.datasource.database}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&useSSL=false - replica1: + replica: driver-class-name: com.mysql.cj.jdbc.Driver - username: ${secret.datasource.replica1.username} - password: ${secret.datasource.replica1.password} - jdbc-url: jdbc:mysql://${secret.datasource.replica1.host}:${secret.datasource.replica1.port}/${secret.datasource.database}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&useSSL=false + username: ${secret.datasource.replica.username} + password: ${secret.datasource.replica.password} + jdbc-url: jdbc:mysql://${secret.datasource.replica.host}:${secret.datasource.replica.port}/${secret.datasource.database}?serverTimezone=Asia/Seoul&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&useSSL=false sql: init: diff --git a/backend/src/test/java/ddangkong/domain/room/balance/roomcontent/RoomContentTest.java b/backend/src/test/java/ddangkong/domain/room/balance/roomcontent/RoomContentTest.java index 20c8b4787..125c0c5a1 100644 --- a/backend/src/test/java/ddangkong/domain/room/balance/roomcontent/RoomContentTest.java +++ b/backend/src/test/java/ddangkong/domain/room/balance/roomcontent/RoomContentTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; import ddangkong.domain.balance.content.BalanceContent; import ddangkong.domain.balance.content.Category; @@ -25,7 +26,7 @@ class 투표_마감_시간_설정 { @Test void 투표_마감_시간을_설정한다() { // given - int currentRound = 1; + int currentRound = 2; int timeLimit = 10_000; RoomSetting roomSetting = new RoomSetting(5, timeLimit, Category.IF); @@ -62,7 +63,7 @@ class 투표_마감_시간_지남_여부 { private static final Room ROOM = Room.createNewRoom(); private static final BalanceContent BALANCE_CONTENT = new BalanceContent(Category.IF, "치킨 vs 피자"); - private static final int ROUND = 1; + private static final int ROUND = 2; @Test void 투표_마감_시간보다_이전_시간이면_투표가_마감되지_않은_것이다() { @@ -100,4 +101,31 @@ class 투표_마감_시간_지남_여부 { .isExactlyInstanceOf(MismatchRoundException.class); } } + + @Nested + class 게임_대기_시간_포함_투표_마감_시간_지남_여부 { + + private static final Room ROOM = Room.createNewRoom(); + private static final BalanceContent BALANCE_CONTENT = new BalanceContent(Category.IF, "치킨 vs 피자"); + private static final int START_ROUND = 1; + + @Test + void 첫_라운드는_투표_마감_시간에는_게임_대기_시간을_추가된다() { + // given + LocalDateTime voteDeadline = LocalDateTime.parse("2024-08-03T20:00:03"); + RoomContent roomContent = RoomContent.newRoomContent(ROOM, BALANCE_CONTENT, START_ROUND); + int timeLimit = 3; + LocalDateTime inTime = LocalDateTime.parse("2024-08-03T20:00:08"); + LocalDateTime overTime = LocalDateTime.parse("2024-08-03T20:00:09"); + + // when + roomContent.updateVoteDeadline(voteDeadline, timeLimit); + + // then + assertAll( + () -> assertThat(roomContent.isOverVoteDeadline(inTime, START_ROUND)).isFalse(), + () -> assertThat(roomContent.isOverVoteDeadline(overTime, START_ROUND)).isTrue() + ); + } + } } diff --git a/frontend/index.html b/frontend/index.html deleted file mode 100644 index bf2da022d..000000000 --- a/frontend/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - ddangkong - - - - - - -
- - - diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 064f918fb..4f6ba7ed8 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -81,6 +81,7 @@ "typescript": "^5.5.3", "undici": "^6.19.2", "webpack": "^5.92.1", + "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4", "webpack-merge": "^6.0.1" @@ -4066,6 +4067,12 @@ "node": ">=14" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true + }, "node_modules/@remix-run/router": { "version": "1.17.1", "license": "MIT", @@ -10227,6 +10234,12 @@ "integrity": "sha512-Rt2g+nTbLlDWZTwwrIXjy9MeiZmSDI375FvZs72ngxx8PDC6YXOeR3q5LAuPzjZQxhiWdRKac7RKV+YyQYfYIg==", "dev": true }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true + }, "node_modules/debug": { "version": "4.3.5", "license": "MIT", @@ -10614,6 +10627,12 @@ "url": "https://dotenvx.com" } }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, "node_modules/eastasianwidth": { "version": "0.2.0", "dev": true, @@ -13037,6 +13056,21 @@ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" } }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/hamt_plus": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/hamt_plus/-/hamt_plus-1.0.2.tgz", @@ -17173,6 +17207,15 @@ "ufo": "^1.5.3" } }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "license": "MIT" @@ -17856,6 +17899,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, "node_modules/optionator": { "version": "0.9.4", "dev": true, @@ -19616,6 +19668,20 @@ "dev": true, "license": "ISC" }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "dev": true, @@ -21076,6 +21142,15 @@ "node": ">=0.6" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "dev": true, @@ -21860,6 +21935,74 @@ } } }, + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", + "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", + "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-bundle-analyzer/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/webpack-cli": { "version": "5.1.4", "dev": true, diff --git a/frontend/package.json b/frontend/package.json index a0473c76e..ab62dc39a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,17 +4,18 @@ "description": "ddangkong-frontend repository", "main": "index.js", "scripts": { - "start": "webpack serve --config webpack.config.dev.js", - "start:open": "webpack serve --config webpack.config.dev.js --host 0.0.0.0", - "build-dev": "webpack --config webpack.config.dev.js", - "build-prod": "webpack --config webpack.config.prod.js", + "dev": "webpack serve --config webpack.config.dev.js", + "dev:open": "webpack serve --config webpack.config.dev.js --host 0.0.0.0", + "prod": "webpack serve --config webpack.config.prod.js", + "build:dev": "webpack --config webpack.config.dev.js", + "build:prod": "webpack --config webpack.config.prod.js", "test": "jest", "test:watch": "jest --watch", "lint": "npx eslint --ext .ts,.tsx .", "lint:styled": "stylelint ./src/**/*.styled.ts --fix", "prepare": "cd .. && husky frontend/.husky", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" + "build:storybook": "storybook build" }, "keywords": [], "author": "", @@ -92,6 +93,7 @@ "typescript": "^5.5.3", "undici": "^6.19.2", "webpack": "^5.92.1", + "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^5.1.4", "webpack-dev-server": "^5.0.4", "webpack-merge": "^6.0.1" diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 000000000..1c647d597 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/public/index.html b/frontend/public/index.html new file mode 100644 index 000000000..a23a5658c --- /dev/null +++ b/frontend/public/index.html @@ -0,0 +1,56 @@ + + + + + + + + 땅콩 - 단체 대화주제 제공 서비스 + + + + + + + + + + + + + + + + + + + + + + +
+ + + diff --git a/frontend/src/assets/images/angryDdangkong.webp b/frontend/src/assets/images/angryDdangkong.webp new file mode 100644 index 000000000..be94c13ff Binary files /dev/null and b/frontend/src/assets/images/angryDdangkong.webp differ diff --git a/frontend/src/assets/images/crownIcon.png b/frontend/src/assets/images/crownIcon.png deleted file mode 100644 index 3c3ccb45d..000000000 Binary files a/frontend/src/assets/images/crownIcon.png and /dev/null differ diff --git a/frontend/src/assets/images/crownIcon.webp b/frontend/src/assets/images/crownIcon.webp new file mode 100644 index 000000000..670a91353 Binary files /dev/null and b/frontend/src/assets/images/crownIcon.webp differ diff --git a/frontend/src/assets/images/ddangkong.webp b/frontend/src/assets/images/ddangkong.webp new file mode 100644 index 000000000..2b5d9d783 Binary files /dev/null and b/frontend/src/assets/images/ddangkong.webp differ diff --git a/frontend/src/assets/images/ddangkongTimer.webp b/frontend/src/assets/images/ddangkongTimer.webp new file mode 100644 index 000000000..2de999427 Binary files /dev/null and b/frontend/src/assets/images/ddangkongTimer.webp differ diff --git a/frontend/src/assets/images/errorDdangkong.webp b/frontend/src/assets/images/errorDdangkong.webp new file mode 100644 index 000000000..d0138bce8 Binary files /dev/null and b/frontend/src/assets/images/errorDdangkong.webp differ diff --git a/frontend/src/assets/images/exitIcon.png b/frontend/src/assets/images/exitIcon.png deleted file mode 100644 index 3efe0d83f..000000000 Binary files a/frontend/src/assets/images/exitIcon.png and /dev/null differ diff --git a/frontend/src/assets/images/exitIcon.webp b/frontend/src/assets/images/exitIcon.webp new file mode 100644 index 000000000..0c8bbd5c3 Binary files /dev/null and b/frontend/src/assets/images/exitIcon.webp differ diff --git a/frontend/src/assets/images/logoIcon.png b/frontend/src/assets/images/logoIcon.png deleted file mode 100644 index b5c81df66..000000000 Binary files a/frontend/src/assets/images/logoIcon.png and /dev/null differ diff --git a/frontend/src/assets/images/plusIcon.png b/frontend/src/assets/images/plusIcon.png deleted file mode 100644 index b85d165ed..000000000 Binary files a/frontend/src/assets/images/plusIcon.png and /dev/null differ diff --git a/frontend/src/assets/images/plusIcon.webp b/frontend/src/assets/images/plusIcon.webp new file mode 100644 index 000000000..404b1bc13 Binary files /dev/null and b/frontend/src/assets/images/plusIcon.webp differ diff --git a/frontend/src/assets/images/sadDdangkong.webp b/frontend/src/assets/images/sadDdangkong.webp new file mode 100644 index 000000000..4261836d3 Binary files /dev/null and b/frontend/src/assets/images/sadDdangkong.webp differ diff --git a/frontend/src/assets/images/settingsIcon.svg b/frontend/src/assets/images/settingsIcon.svg deleted file mode 100644 index 19c27265a..000000000 --- a/frontend/src/assets/images/settingsIcon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/images/settingsIcon.webp b/frontend/src/assets/images/settingsIcon.webp new file mode 100644 index 000000000..5643873ee Binary files /dev/null and b/frontend/src/assets/images/settingsIcon.webp differ diff --git a/frontend/src/assets/images/sillyDdangkong.png b/frontend/src/assets/images/sillyDdangkong.png deleted file mode 100644 index 3280912c5..000000000 Binary files a/frontend/src/assets/images/sillyDdangkong.png and /dev/null differ diff --git a/frontend/src/assets/images/sillyDdangkong.webp b/frontend/src/assets/images/sillyDdangkong.webp new file mode 100644 index 000000000..13b1afd8b Binary files /dev/null and b/frontend/src/assets/images/sillyDdangkong.webp differ diff --git a/frontend/src/assets/images/spinDdangkong.webp b/frontend/src/assets/images/spinDdangkong.webp new file mode 100644 index 000000000..167c9a577 Binary files /dev/null and b/frontend/src/assets/images/spinDdangkong.webp differ diff --git a/frontend/src/components/GameResult/GameResult.styled.ts b/frontend/src/components/GameResult/GameResult.styled.ts index ff4f99a21..ccc03d8b9 100644 --- a/frontend/src/components/GameResult/GameResult.styled.ts +++ b/frontend/src/components/GameResult/GameResult.styled.ts @@ -36,6 +36,7 @@ export const noMatchingLayout = css` export const noMatchingImg = css` width: 18rem; + height: 30vh; `; export const noMatchingText = css` diff --git a/frontend/src/components/GameResult/GameResult.tsx b/frontend/src/components/GameResult/GameResult.tsx index 81e76222f..d2ba8b055 100644 --- a/frontend/src/components/GameResult/GameResult.tsx +++ b/frontend/src/components/GameResult/GameResult.tsx @@ -15,7 +15,7 @@ import GameResultItem from '../GameResultItem/GameResultItem'; import ArrowDown from '@/assets/images/arrowDown.svg'; import ArrowUp from '@/assets/images/arrowUp.svg'; -import SadDdangKong from '@/assets/images/sadDdangkong.png'; +import SadDdangKong from '@/assets/images/sadDdangkong.webp'; const GameResult = () => { const { matchedMembers, existMatching, isLoading } = useMatchingResultQuery(); diff --git a/frontend/src/components/GameResultItem/GameResultItem.styled.ts b/frontend/src/components/GameResultItem/GameResultItem.styled.ts index 2d00eed72..1317dfed3 100644 --- a/frontend/src/components/GameResultItem/GameResultItem.styled.ts +++ b/frontend/src/components/GameResultItem/GameResultItem.styled.ts @@ -38,7 +38,8 @@ export const nicknameContainer = (percent: number) => css` `; export const rankPercent = css` - ${Theme.typography.headline3} + width: 1.6rem; + ${Theme.typography.headline3}; `; export const nickname = css` diff --git a/frontend/src/components/GameResultItem/GameResultItem.tsx b/frontend/src/components/GameResultItem/GameResultItem.tsx index 2b190f3d5..fcf4adace 100644 --- a/frontend/src/components/GameResultItem/GameResultItem.tsx +++ b/frontend/src/components/GameResultItem/GameResultItem.tsx @@ -8,7 +8,7 @@ import { rankPercent, } from './GameResultItem.styled'; -import SillyDdangkong from '@/assets/images/sillyDdangkong.png'; +import SillyDdangkong from '@/assets/images/sillyDdangkong.webp'; import useCountAnimation from '@/hooks/useCountAnimation'; import { MemberMatchingInfo } from '@/types/balanceContent'; diff --git a/frontend/src/components/NicknameItem/NicknameItem.tsx b/frontend/src/components/NicknameItem/NicknameItem.tsx index 32ebd1fbd..1568456aa 100644 --- a/frontend/src/components/NicknameItem/NicknameItem.tsx +++ b/frontend/src/components/NicknameItem/NicknameItem.tsx @@ -2,9 +2,8 @@ import { useRecoilValue } from 'recoil'; import { nicknameItemLayout, nicknameText, profileImage } from './NicknameItem.styled'; -import SillyDdangkong from '@/assets/images/sillyDdangkong.png'; +import SillyDdangkong from '@/assets/images/sillyDdangkong.webp'; import { memberInfoState } from '@/recoil/atom'; - interface NicknameItemProp { nickName: string; } diff --git a/frontend/src/components/ReadyMembersContainer/ReadyMembersContainer.tsx b/frontend/src/components/ReadyMembersContainer/ReadyMembersContainer.tsx index 89ba6db97..6ac92c990 100644 --- a/frontend/src/components/ReadyMembersContainer/ReadyMembersContainer.tsx +++ b/frontend/src/components/ReadyMembersContainer/ReadyMembersContainer.tsx @@ -14,8 +14,8 @@ import { } from './ReadyMembersContainer.styled'; import InviteModal from '../common/InviteModal/InviteModal'; -import crownIcon from '@/assets/images/crownIcon.png'; -import SillyDdangkong from '@/assets/images/sillyDdangkong.png'; +import crownIcon from '@/assets/images/crownIcon.webp'; +import SillyDdangkong from '@/assets/images/sillyDdangkong.webp'; import { useGetRoomInfo } from '@/hooks/useGetRoomInfo'; import useModal from '@/hooks/useModal'; import { memberInfoState } from '@/recoil/atom'; diff --git a/frontend/src/components/SelectContainer/Timer/Timer.styled.ts b/frontend/src/components/SelectContainer/Timer/Timer.styled.ts index 8e7fa1b29..54244a4de 100644 --- a/frontend/src/components/SelectContainer/Timer/Timer.styled.ts +++ b/frontend/src/components/SelectContainer/Timer/Timer.styled.ts @@ -42,28 +42,36 @@ export const timerLayout = css` box-sizing: border-box; `; -export const timerInnerLayout = (width: number) => css` +export const timerInnerLayout = (scale: number) => css` display: flex; justify-content: center; align-items: center; - width: ${width}%; + + width: 100%; height: 60%; border-radius: ${Theme.borderRadius.radius30}; background-color: ${Theme.color.peanut500}; - transition: width 1s linear; + + transform: scaleX(${scale}); + transform-origin: left; + + transition: transform 1s linear; `; -export const timerWrapper = (width: number) => css` +// 화면을 벗어나는 문제로 인해 100이 아닌 98로 계산 +export const timerWrapper = (scale: number) => css` display: flex; position: absolute; - right: ${100 - width}%; flex-direction: column; justify-content: center; - align-items: center; - gap: 2rem; + align-items: flex-end; + + width: 100%; height: 4rem; - transition: all 1s linear; + + transform: translateX(-${(1 - scale) * 98}%); + transition: transform 1s linear; `; export const timerIcon = css` diff --git a/frontend/src/components/SelectContainer/Timer/Timer.test.tsx b/frontend/src/components/SelectContainer/Timer/Timer.test.tsx index 0e8affc6e..c9396ca89 100644 --- a/frontend/src/components/SelectContainer/Timer/Timer.test.tsx +++ b/frontend/src/components/SelectContainer/Timer/Timer.test.tsx @@ -30,8 +30,6 @@ describe('Timer 테스트', () => { useTimer({ timeLimit, isSelectedOption, isVoted, vote: voteMock }), ); - // act : 인자로 받은 함수를 실행시켜서 가상의 DOM(jsdom)에 적용하는 역할 - // 상태 변경과 그로 인한 DOM 업데이트가 모두 완료된 후에 테스트가 실행되도록 보장 act(() => { jest.advanceTimersByTime(timeLimit); }); diff --git a/frontend/src/components/SelectContainer/Timer/Timer.tsx b/frontend/src/components/SelectContainer/Timer/Timer.tsx index 29bb88ac9..e73c57034 100644 --- a/frontend/src/components/SelectContainer/Timer/Timer.tsx +++ b/frontend/src/components/SelectContainer/Timer/Timer.tsx @@ -12,7 +12,7 @@ import { import { formatLeftRoundTime } from './Timer.util'; import useVoteIsFinished from '../hooks/useVoteIsFinished'; -import DdangkongTimer from '@/assets/images/ddangkongTimer.png'; +import DdangkongTimer from '@/assets/images/ddangkongTimer.webp'; import useBalanceContentQuery from '@/hooks/useBalanceContentQuery'; interface TimerProps { @@ -24,7 +24,7 @@ interface TimerProps { const Timer = ({ selectedId, isVoted, completeSelection }: TimerProps) => { const { roomId } = useParams(); const { balanceContent, isFetching } = useBalanceContentQuery(Number(roomId)); - const { barWidthPercent, leftRoundTime, isAlmostFinished } = useVoteTimer({ + const { leftRoundTime, barScaleRate, isAlmostFinished } = useVoteTimer({ roomId: Number(roomId), selectedId, isVoted, @@ -38,8 +38,8 @@ const Timer = ({ selectedId, isVoted, completeSelection }: TimerProps) => { return (
-
-
+
+
{ return msec / UNIT_MSEC; }; -export const calculateUnitRatio = (total: number, divisor: number) => { - return total / divisor; +export const calculateUnitRate = (total: number, divisor: number) => { + return parseFloat((total / divisor).toFixed(1)); }; diff --git a/frontend/src/components/SelectContainer/Timer/hooks/useTimer.ts b/frontend/src/components/SelectContainer/Timer/hooks/useTimer.ts index 20a68a2d3..f3f3ebf6e 100644 --- a/frontend/src/components/SelectContainer/Timer/hooks/useTimer.ts +++ b/frontend/src/components/SelectContainer/Timer/hooks/useTimer.ts @@ -1,10 +1,10 @@ import { useEffect, useRef, useState } from 'react'; -import { calculateUnitRatio, convertMsecToSecond } from '../Timer.util'; +import { calculateUnitRate, convertMsecToSecond } from '../Timer.util'; import { POLLING_DELAY } from '@/constants/config'; -const INITIAL_WIDTH = 100; +const INITIAL_BAR_SCALE = 1; const ALMOST_FINISH_SECOND = 5; interface UseTimerProps { @@ -16,7 +16,7 @@ interface UseTimerProps { const useTimer = ({ timeLimit, isSelectedOption, isVoted, vote }: UseTimerProps) => { const [leftRoundTime, setLeftRoundTime] = useState(convertMsecToSecond(timeLimit)); - const [barWidthPercent, setBarWidthPercent] = useState(INITIAL_WIDTH); + const [barScaleRate, setBarScaleRate] = useState(INITIAL_BAR_SCALE); const isVoteTimeout = leftRoundTime <= 0; const isAlmostFinished = leftRoundTime <= ALMOST_FINISH_SECOND; @@ -35,12 +35,12 @@ const useTimer = ({ timeLimit, isSelectedOption, isVoted, vote }: UseTimerProps) useEffect(() => { const timeLimitPerSecond = convertMsecToSecond(timeLimit); - const DECREASE_PERCENT = calculateUnitRatio(INITIAL_WIDTH, timeLimitPerSecond); + const decreaseRate = calculateUnitRate(INITIAL_BAR_SCALE, timeLimitPerSecond); setLeftRoundTime(timeLimitPerSecond); timeout.current = setInterval(() => { setLeftRoundTime((prev) => prev - 1); - setBarWidthPercent((prevWidth) => (prevWidth > 0 ? prevWidth - DECREASE_PERCENT : 0)); + setBarScaleRate((prevRate) => (prevRate > 0 ? prevRate - decreaseRate : 0)); }, POLLING_DELAY); return () => { @@ -48,7 +48,7 @@ const useTimer = ({ timeLimit, isSelectedOption, isVoted, vote }: UseTimerProps) }; }, [timeLimit]); - return { leftRoundTime, barWidthPercent, isAlmostFinished }; + return { leftRoundTime, barScaleRate, isAlmostFinished }; }; export default useTimer; diff --git a/frontend/src/components/SelectContainer/Timer/hooks/useVoteTimer.ts b/frontend/src/components/SelectContainer/Timer/hooks/useVoteTimer.ts index 71bf760a7..6161d777e 100644 --- a/frontend/src/components/SelectContainer/Timer/hooks/useVoteTimer.ts +++ b/frontend/src/components/SelectContainer/Timer/hooks/useVoteTimer.ts @@ -22,14 +22,14 @@ const useVoteTimer = ({ roomId, selectedId, isVoted, completeSelection }: UseVot completeSelection, }); - const { leftRoundTime, barWidthPercent, isAlmostFinished } = useTimer({ + const { leftRoundTime, barScaleRate, isAlmostFinished } = useTimer({ timeLimit, isSelectedOption: Boolean(selectedId), isVoted, vote, }); - return { leftRoundTime, barWidthPercent, isAlmostFinished }; + return { leftRoundTime, barScaleRate, isAlmostFinished }; }; export default useVoteTimer; diff --git a/frontend/src/components/StartButtonContainer/Countdown/Countdown.tsx b/frontend/src/components/StartButtonContainer/Countdown/Countdown.tsx index 9f4962a6e..f7a30ec3f 100644 --- a/frontend/src/components/StartButtonContainer/Countdown/Countdown.tsx +++ b/frontend/src/components/StartButtonContainer/Countdown/Countdown.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react'; import { countdownLayout, countdown, peanut, imageContainer, dimmed } from './Countdown.styled'; -import SpinDdangkong from '@/assets/images/spinDdangkong.png'; +import SpinDdangkong from '@/assets/images/spinDdangkong.webp'; const START_COUNTDOWN = 3; diff --git a/frontend/src/components/TabContentContainer/TabContentContainer.styled.ts b/frontend/src/components/TabContentContainer/TabContentContainer.styled.ts index 564df2429..667d8a9ee 100644 --- a/frontend/src/components/TabContentContainer/TabContentContainer.styled.ts +++ b/frontend/src/components/TabContentContainer/TabContentContainer.styled.ts @@ -7,7 +7,7 @@ export const contentWrapperStyle = css` flex-direction: column; gap: 15%; height: 50vh; - overflow-y: scroll; + overflow-y: auto; padding: 2.4rem; border: 0.3rem solid ${Theme.color.peanut400}; border-radius: 0.8rem; diff --git a/frontend/src/components/TabContentContainer/TabContentContainer.tsx b/frontend/src/components/TabContentContainer/TabContentContainer.tsx index 85c94a8aa..8030bd4a1 100644 --- a/frontend/src/components/TabContentContainer/TabContentContainer.tsx +++ b/frontend/src/components/TabContentContainer/TabContentContainer.tsx @@ -20,7 +20,7 @@ import OptionParticipantsContainer from '../OptionParticipantsContainer/OptionPa import useTotalCountAnimation from '../RoundVoteContainer/RoundVoteContainer.hook'; import TopicContainer from '../TopicContainer/TopicContainer'; -import AngryDdangkong from '@/assets/images/angryDdangkong.png'; +import AngryDdangkong from '@/assets/images/angryDdangkong.webp'; import useBalanceContentQuery from '@/hooks/useBalanceContentQuery'; import useMyGameStatus from '@/hooks/useMyGameStatus'; import useRoundVoteResultQuery from '@/hooks/useRoundVoteResultQuery'; diff --git a/frontend/src/components/common/ErrorBoundary/AsyncErrorBoundary.tsx b/frontend/src/components/common/ErrorBoundary/AsyncErrorBoundary.tsx index 8cfe52d02..60c7136a7 100644 --- a/frontend/src/components/common/ErrorBoundary/AsyncErrorBoundary.tsx +++ b/frontend/src/components/common/ErrorBoundary/AsyncErrorBoundary.tsx @@ -4,15 +4,16 @@ import { PropsWithChildren, Suspense } from 'react'; import DeferredComponent from '../DeferredComponent/DeferredComponent'; import AsyncErrorFallback from '../ErrorFallback/AsyncErrorFallback/AsyncErrorFallback'; +import Spinner from '../Spinner/Spinner'; import { CustomError } from '@/utils/error'; interface AsyncErrorBoundaryProps { - pendingFallback: React.ReactNode; + pendingFallback?: React.ReactNode; } const AsyncErrorBoundary = ({ - pendingFallback, + pendingFallback = , children, }: PropsWithChildren) => { const { reset } = useQueryErrorResetBoundary(); diff --git a/frontend/src/components/common/ErrorFallback/AsyncErrorFallback/AsyncErrorFallback.tsx b/frontend/src/components/common/ErrorFallback/AsyncErrorFallback/AsyncErrorFallback.tsx index 2d9c5c14c..a8b895ce6 100644 --- a/frontend/src/components/common/ErrorFallback/AsyncErrorFallback/AsyncErrorFallback.tsx +++ b/frontend/src/components/common/ErrorFallback/AsyncErrorFallback/AsyncErrorFallback.tsx @@ -8,7 +8,7 @@ import { fallbackButtonContainer, } from '../ErrorFallback.styled'; -import ErrorDdangkong from '@/assets/images/errorDdangkong.png'; +import ErrorDdangkong from '@/assets/images/errorDdangkong.webp'; import { CustomError } from '@/utils/error'; interface AsyncErrorFallback { diff --git a/frontend/src/components/common/ErrorFallback/RootErrorFallback/RootErrorFallback.tsx b/frontend/src/components/common/ErrorFallback/RootErrorFallback/RootErrorFallback.tsx index 94f3b3592..275aeaf3b 100644 --- a/frontend/src/components/common/ErrorFallback/RootErrorFallback/RootErrorFallback.tsx +++ b/frontend/src/components/common/ErrorFallback/RootErrorFallback/RootErrorFallback.tsx @@ -9,7 +9,7 @@ import { fallbackButtonContainer, } from '../ErrorFallback.styled'; -import ErrorDdangkong from '@/assets/images/errorDdangkong.png'; +import ErrorDdangkong from '@/assets/images/errorDdangkong.webp'; interface RootErrorFallbackProps { error: unknown; diff --git a/frontend/src/components/common/ErrorFallback/RouterErrorFallback/RouterErrorFallback.tsx b/frontend/src/components/common/ErrorFallback/RouterErrorFallback/RouterErrorFallback.tsx index 650538a4f..c499ab324 100644 --- a/frontend/src/components/common/ErrorFallback/RouterErrorFallback/RouterErrorFallback.tsx +++ b/frontend/src/components/common/ErrorFallback/RouterErrorFallback/RouterErrorFallback.tsx @@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom'; import Button from '../../Button/Button'; import { errorFallbackLayout, errorImage, errorText } from '../ErrorFallback.styled'; -import ErrorDdangkong from '@/assets/images/errorDdangkong.png'; +import ErrorDdangkong from '@/assets/images/errorDdangkong.webp'; const RouterErrorFallback = () => { const navigate = useNavigate(); diff --git a/frontend/src/components/common/Spinner/Spinner.styled.ts b/frontend/src/components/common/Spinner/Spinner.styled.ts index 1ddc9dbec..44c3840c3 100644 --- a/frontend/src/components/common/Spinner/Spinner.styled.ts +++ b/frontend/src/components/common/Spinner/Spinner.styled.ts @@ -11,7 +11,7 @@ export const spinnerWrapper = css` export const rotatingImage = (size: number) => css` width: ${size}rem; - height: auto; + height: 20vh; animation: spin 2s linear infinite; /* 2초 동안 한 바퀴 회전하는 애니메이션 */ diff --git a/frontend/src/components/common/Spinner/Spinner.tsx b/frontend/src/components/common/Spinner/Spinner.tsx index b9836186e..f47f06e26 100644 --- a/frontend/src/components/common/Spinner/Spinner.tsx +++ b/frontend/src/components/common/Spinner/Spinner.tsx @@ -1,6 +1,6 @@ import { spinnerWrapper, rotatingImage, spinnerText } from './Spinner.styled'; -import SpinDdangKong from '@/assets/images/spinDdangkong.png'; +import SpinDdangKong from '@/assets/images/spinDdangkong.webp'; interface SpinnerProps { message?: string; diff --git a/frontend/src/components/layout/Header/Header.tsx b/frontend/src/components/layout/Header/Header.tsx index 49dad4e15..61d4219c1 100644 --- a/frontend/src/components/layout/Header/Header.tsx +++ b/frontend/src/components/layout/Header/Header.tsx @@ -16,8 +16,8 @@ import { useExit } from './hooks/useExit'; import useRoutePath from './hooks/useRoutePath'; import ArrowLeft from '@/assets/images/arrowLeft.svg'; -import ExitIcon from '@/assets/images/exitIcon.png'; -import SettingIcon from '@/assets/images/settingsIcon.svg'; +import ExitIcon from '@/assets/images/exitIcon.webp'; +import SettingIcon from '@/assets/images/settingsIcon.webp'; import RoomSettingModal from '@/components/common/RoomSettingModal/RoomSettingModal'; import { ROUTES } from '@/constants/routes'; import useBalanceContentQuery from '@/hooks/useBalanceContentQuery'; diff --git a/frontend/src/custom.d.ts b/frontend/src/custom.d.ts index ccb762825..0930b7d86 100644 --- a/frontend/src/custom.d.ts +++ b/frontend/src/custom.d.ts @@ -3,3 +3,4 @@ declare module '*.png'; declare module '*.jpeg'; declare module '*.gif'; declare module '*.svg'; +declare module '*.webp'; diff --git a/frontend/src/pages/GamePage/GamePage.tsx b/frontend/src/pages/GamePage/GamePage.tsx index eb27070df..10aacfc65 100644 --- a/frontend/src/pages/GamePage/GamePage.tsx +++ b/frontend/src/pages/GamePage/GamePage.tsx @@ -1,5 +1,3 @@ -import AsyncErrorBoundary from '@/components/common/ErrorBoundary/AsyncErrorBoundary'; -import GameSkeleton from '@/components/common/Skeleton/GameSkeleton/GameSkeleton'; import Content from '@/components/layout/Content/Content'; import { RoundHeader } from '@/components/layout/Header/Header'; import SelectContainer from '@/components/SelectContainer/SelectContainer'; @@ -7,13 +5,13 @@ import TopicContainer from '@/components/TopicContainer/TopicContainer'; const GamePage = () => { return ( - }> + <> - + ); }; diff --git a/frontend/src/pages/MainPage/MainPage.tsx b/frontend/src/pages/MainPage/MainPage.tsx index ce0230b42..0f39c0303 100644 --- a/frontend/src/pages/MainPage/MainPage.tsx +++ b/frontend/src/pages/MainPage/MainPage.tsx @@ -9,7 +9,7 @@ import { } from './MainPage.styled'; import { useCreateRoom } from './useCreateRoom'; -import Ddangkong from '@/assets/images/ddangkong.png'; +import Ddangkong from '@/assets/images/ddangkong.webp'; import Button from '@/components/common/Button/Button'; const MainPage = () => { @@ -18,7 +18,7 @@ const MainPage = () => { return (
- 로고 아이콘 + 땅콩 로고

땅콩

diff --git a/frontend/src/pages/NicknamePage/NicknamePage.tsx b/frontend/src/pages/NicknamePage/NicknamePage.tsx index 321dab663..a54c5c2dd 100644 --- a/frontend/src/pages/NicknamePage/NicknamePage.tsx +++ b/frontend/src/pages/NicknamePage/NicknamePage.tsx @@ -16,12 +16,11 @@ import { import useMakeOrEnterRoom from './useMakeOrEnterRoom'; import { isJoinableRoom } from '@/apis/room'; -import AngryDdangkong from '@/assets/images/angryDdangkong.png'; -import SillyDdangkong from '@/assets/images/sillyDdangkong.png'; +import AngryDdangkong from '@/assets/images/angryDdangkong.webp'; +import SillyDdangkong from '@/assets/images/sillyDdangkong.webp'; import Button from '@/components/common/Button/Button'; import Content from '@/components/layout/Content/Content'; import useKeyboardUp from '@/hooks/useKeyboardUp'; -import useModal from '@/hooks/useModal'; import { roomUuidState } from '@/recoil/atom'; const NicknamePage = () => { diff --git a/frontend/src/pages/ReadyPage/ReadyPage.tsx b/frontend/src/pages/ReadyPage/ReadyPage.tsx index 4a0f61e06..cf28fbf16 100644 --- a/frontend/src/pages/ReadyPage/ReadyPage.tsx +++ b/frontend/src/pages/ReadyPage/ReadyPage.tsx @@ -1,19 +1,15 @@ import CategoryContainer from '@/components/CategoryContainer/CategoryContainer'; -import AsyncErrorBoundary from '@/components/common/ErrorBoundary/AsyncErrorBoundary'; -import ReadySkeleton from '@/components/common/Skeleton/ReadySkeleton/ReadySkeleton'; import Content from '@/components/layout/Content/Content'; import ReadyMembersContainer from '@/components/ReadyMembersContainer/ReadyMembersContainer'; import StartButtonContainer from '@/components/StartButtonContainer/StartButtonContainer'; const ReadyPage = () => { return ( - }> - - - - - - + + + + + ); }; diff --git a/frontend/src/router/index.tsx b/frontend/src/router/index.tsx index 6da82e6be..96f391c36 100644 --- a/frontend/src/router/index.tsx +++ b/frontend/src/router/index.tsx @@ -1,15 +1,20 @@ import { createBrowserRouter } from 'react-router-dom'; import HeaderLayout from './HeaderLayout'; +import { + GamePage, + GameResultPage, + MainPage, + NicknamePage, + ReadyPage, + RoundResultPage, +} from './lazyPages'; import MainLayout from './MainLayout'; +import AsyncErrorBoundary from '@/components/common/ErrorBoundary/AsyncErrorBoundary'; import RouterErrorFallback from '@/components/common/ErrorFallback/RouterErrorFallback/RouterErrorFallback'; -import GamePage from '@/pages/GamePage/GamePage'; -import GameResultPage from '@/pages/GameResultPage/GameResultPage'; -import MainPage from '@/pages/MainPage/MainPage'; -import NicknamePage from '@/pages/NicknamePage/NicknamePage'; -import ReadyPage from '@/pages/ReadyPage/ReadyPage'; -import RoundResultPage from '@/pages/RoundResultPage/RoundResultPage'; +import GameSkeleton from '@/components/common/Skeleton/GameSkeleton/GameSkeleton'; +import ReadySkeleton from '@/components/common/Skeleton/ReadySkeleton/ReadySkeleton'; export const router = createBrowserRouter([ { @@ -29,21 +34,45 @@ export const router = createBrowserRouter([ path: '/', element: , children: [ + { + path: ':roomId/game', + element: ( + }> + + + ), + }, { path: 'nickname/:roomUuid?', - element: , + element: ( + + , + + ), }, { path: ':roomId/ready', - element: , + element: ( + }> + + + ), }, { path: ':roomId/round/result', - element: , + element: ( + + + + ), }, { path: ':roomId/game/result', - element: , + element: ( + + + + ), }, ], }, diff --git a/frontend/src/router/lazyPages.ts b/frontend/src/router/lazyPages.ts new file mode 100644 index 000000000..55a34ce86 --- /dev/null +++ b/frontend/src/router/lazyPages.ts @@ -0,0 +1,8 @@ +import { lazy } from 'react'; + +export const GamePage = lazy(() => import('@/pages/GamePage/GamePage')); +export const GameResultPage = lazy(() => import('@/pages/GameResultPage/GameResultPage')); +export const MainPage = lazy(() => import('@/pages/MainPage/MainPage')); +export const NicknamePage = lazy(() => import('@/pages/NicknamePage/NicknamePage')); +export const ReadyPage = lazy(() => import('@/pages/ReadyPage/ReadyPage')); +export const RoundResultPage = lazy(() => import('@/pages/RoundResultPage/RoundResultPage')); diff --git a/frontend/webpack.config.common.js b/frontend/webpack.config.common.js index 9a518ee3c..195ee52f2 100644 --- a/frontend/webpack.config.common.js +++ b/frontend/webpack.config.common.js @@ -8,7 +8,7 @@ dotenv.config(); module.exports = { entry: './src/index.tsx', output: { - filename: 'bundle.js', + filename: '[name].[contenthash].js', path: path.resolve(__dirname + '/dist'), publicPath: '/', clean: true, @@ -30,7 +30,7 @@ module.exports = { ], }, { - test: /\.(png|jpe?g|gif|svg)$/i, + test: /\.(png|jpe?g|gif|svg|webp)$/i, use: [ { loader: 'file-loader', @@ -44,8 +44,9 @@ module.exports = { }, plugins: [ new HTMLWebpackPlugin({ - template: './index.html', // 읽을 파일명 - filename: './index.html', // output으로 출력할 파일명 + template: path.join(__dirname, './public/index.html'), + hash: true, + favicon: path.join(__dirname, './public/favicon.ico'), }), new webpack.DefinePlugin({ 'process.env': JSON.stringify(process.env),