├── server ├── docs │ └── migration.sql ├── settings.gradle ├── src │ ├── main │ │ └── java │ │ │ └── haengdong │ │ │ ├── user │ │ │ ├── domain │ │ │ │ ├── Role.java │ │ │ │ └── repository │ │ │ │ │ └── UserRepository.java │ │ │ ├── application │ │ │ │ ├── UserDeleteEvent.java │ │ │ │ ├── response │ │ │ │ │ └── KakaoTokenResponse.java │ │ │ │ └── request │ │ │ │ │ ├── UserJoinAppRequest.java │ │ │ │ │ ├── UserGuestSaveAppRequest.java │ │ │ │ │ └── UserUpdateAppRequest.java │ │ │ ├── presentation │ │ │ │ ├── response │ │ │ │ │ └── KakaoClientId.java │ │ │ │ └── request │ │ │ │ │ ├── UserUpdateRequest.java │ │ │ │ │ └── UserGuestSaveRequest.java │ │ │ └── config │ │ │ │ └── KakaoProperties.java │ │ │ ├── event │ │ │ ├── application │ │ │ │ ├── request │ │ │ │ │ ├── EventAppRequest.java │ │ │ │ │ ├── EventLoginAppRequest.java │ │ │ │ │ ├── BillUpdateAppRequest.java │ │ │ │ │ ├── MemberNameUpdateAppRequest.java │ │ │ │ │ ├── MemberSaveAppRequest.java │ │ │ │ │ ├── EventDeleteAppRequest.java │ │ │ │ │ ├── MemberNamesUpdateAppRequest.java │ │ │ │ │ ├── MemberUpdateAppRequest.java │ │ │ │ │ ├── MembersUpdateAppRequest.java │ │ │ │ │ ├── EventUpdateAppRequest.java │ │ │ │ │ ├── BillAppRequest.java │ │ │ │ │ ├── BillDetailsUpdateAppRequest.java │ │ │ │ │ ├── EventGuestAppRequest.java │ │ │ │ │ ├── EventMineAppResponse.java │ │ │ │ │ └── MembersSaveAppRequest.java │ │ │ │ └── response │ │ │ │ │ ├── EventImageAppResponse.java │ │ │ │ │ ├── EventImageUrlAppResponse.java │ │ │ │ │ ├── ImageInfo.java │ │ │ │ │ ├── MemberBillReportAppResponse.java │ │ │ │ │ ├── EventAppResponse.java │ │ │ │ │ ├── EventImageSaveAppResponse.java │ │ │ │ │ ├── BillAppResponse.java │ │ │ │ │ ├── LastBillMemberAppResponse.java │ │ │ │ │ ├── MemberAppResponse.java │ │ │ │ │ ├── MemberSaveAppResponse.java │ │ │ │ │ ├── MemberDepositAppResponse.java │ │ │ │ │ ├── MembersDepositAppResponse.java │ │ │ │ │ ├── MembersSaveAppResponse.java │ │ │ │ │ ├── BillDetailsAppResponse.java │ │ │ │ │ ├── EventDetailAppResponse.java │ │ │ │ │ ├── BillDetailAppResponse.java │ │ │ │ │ ├── UserAppResponse.java │ │ │ │ │ └── StepAppResponse.java │ │ │ ├── domain │ │ │ │ ├── RandomValueProvider.java │ │ │ │ ├── event │ │ │ │ │ ├── member │ │ │ │ │ │ └── EventMemberRepository.java │ │ │ │ │ ├── EventRepository.java │ │ │ │ │ └── image │ │ │ │ │ │ └── EventImageRepository.java │ │ │ │ └── bill │ │ │ │ │ └── BillRepository.java │ │ │ ├── presentation │ │ │ │ ├── request │ │ │ │ │ ├── EventDeleteRequest.java │ │ │ │ │ ├── EventSaveRequest.java │ │ │ │ │ ├── MemberSaveRequest.java │ │ │ │ │ ├── EventUpdateRequest.java │ │ │ │ │ ├── EventLoginRequest.java │ │ │ │ │ ├── BillUpdateRequest.java │ │ │ │ │ ├── BillDetailUpdateRequest.java │ │ │ │ │ ├── MembersSaveRequest.java │ │ │ │ │ ├── MembersUpdateRequest.java │ │ │ │ │ ├── EventGuestSaveRequest.java │ │ │ │ │ ├── BillDetailsUpdateRequest.java │ │ │ │ │ ├── BillSaveRequest.java │ │ │ │ │ └── MemberUpdateRequest.java │ │ │ │ └── response │ │ │ │ │ ├── EventResponse.java │ │ │ │ │ ├── EventImageResponse.java │ │ │ │ │ ├── MemberSaveResponse.java │ │ │ │ │ ├── StepsResponse.java │ │ │ │ │ ├── BillResponse.java │ │ │ │ │ ├── MemberDepositResponse.java │ │ │ │ │ ├── MembersResponse.java │ │ │ │ │ ├── EventImagesResponse.java │ │ │ │ │ ├── CurrentMembersResponse.java │ │ │ │ │ ├── EventsMineResponse.java │ │ │ │ │ ├── MembersSaveResponse.java │ │ │ │ │ ├── EventMineResponse.java │ │ │ │ │ ├── MemberBillReportsResponse.java │ │ │ │ │ ├── BillDetailsResponse.java │ │ │ │ │ ├── MemberResponse.java │ │ │ │ │ ├── BillDetailResponse.java │ │ │ │ │ ├── StepResponse.java │ │ │ │ │ ├── MemberBillReportResponse.java │ │ │ │ │ └── EventDetailResponse.java │ │ │ ├── properties │ │ │ │ └── ImageProperties.java │ │ │ └── infrastructure │ │ │ │ └── UUIDProvider.java │ │ │ ├── common │ │ │ ├── properties │ │ │ │ ├── JwtProperties.java │ │ │ │ ├── CorsProperties.java │ │ │ │ └── CookieProperties.java │ │ │ ├── config │ │ │ │ └── JpaConfig.java │ │ │ ├── auth │ │ │ │ ├── TokenProvider.java │ │ │ │ └── Login.java │ │ │ ├── exception │ │ │ │ ├── ErrorResponse.java │ │ │ │ ├── AuthenticationException.java │ │ │ │ └── HaengdongException.java │ │ │ ├── infrastructure │ │ │ │ └── DynamicRoutingDataSource.java │ │ │ └── domain │ │ │ │ └── BaseEntity.java │ │ │ └── HaengdongApplication.java │ ├── docs │ │ └── asciidoc │ │ │ ├── memberBillReport.adoc │ │ │ └── index.adoc │ └── test │ │ └── java │ │ └── haengdong │ │ ├── application │ │ └── ServiceTestSupport.java │ │ ├── domain │ │ └── event │ │ │ └── PasswordTest.java │ │ └── support │ │ └── extension │ │ └── DatabaseCleanerExtension.java ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties └── Dockerfile ├── client ├── cypress │ ├── support │ │ └── e2e.ts │ ├── fixtures │ │ └── postEvent.json │ └── constants │ │ └── constants.ts ├── .npmrc ├── src │ ├── constants │ │ ├── password.ts │ │ ├── message.ts │ │ ├── regExp.ts │ │ ├── rule.ts │ │ ├── queryKeys.ts │ │ └── sessionStorageKeys.ts │ ├── pages │ │ ├── landing │ │ │ ├── Nav │ │ │ │ ├── index.ts │ │ │ │ └── Nav.style.ts │ │ │ ├── index.ts │ │ │ ├── Section │ │ │ │ ├── MainSection │ │ │ │ │ └── index.ts │ │ │ │ ├── FeatureSection │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── SimpleShare │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── AutoCalculate │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── CheckDeposit │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── SimpleTransfer │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── RecordMemoryWithPhoto │ │ │ │ │ │ └── index.ts │ │ │ │ ├── DescriptionSection │ │ │ │ │ ├── index.ts │ │ │ │ │ └── DescriptionSection.style.ts │ │ │ │ └── CreatorSection │ │ │ │ │ └── Avatar.style.ts │ │ │ └── LandingPage.style.ts │ │ ├── event │ │ │ ├── [eventId] │ │ │ │ ├── home │ │ │ │ │ ├── index.ts │ │ │ │ │ └── HomePage.style.ts │ │ │ │ ├── admin │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── AuthGate.tsx │ │ │ │ │ ├── AdminPage.style.ts │ │ │ │ │ └── members │ │ │ │ │ │ └── MemberPageType.ts │ │ │ │ ├── index.ts │ │ │ │ └── qrcode │ │ │ │ │ └── QRCodePage.style.ts │ │ │ └── create │ │ │ │ └── guest │ │ │ │ └── index.ts │ │ ├── login │ │ │ └── LoginPage.style.ts │ │ ├── setting │ │ │ ├── SettingPage.type.ts │ │ │ └── withdraw │ │ │ │ └── ReasonStep.style.ts │ │ ├── fallback │ │ │ ├── BillEmptyFallback.tsx │ │ │ ├── MainPageLoading.tsx │ │ │ ├── SendErrorPage.tsx │ │ │ ├── EventEmptyFallback.tsx │ │ │ └── EventPageLoading.tsx │ │ └── main │ │ │ └── edit-account │ │ │ └── EditUserAccountPage.tsx │ ├── components │ │ ├── Footer │ │ │ ├── index.ts │ │ │ ├── Footer.type.ts │ │ │ └── Footer.style.ts │ │ ├── Design │ │ │ ├── components │ │ │ │ ├── Banner │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── Banner.type.ts │ │ │ │ │ └── Banner.stories.tsx │ │ │ │ ├── Select │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── Select.type.ts │ │ │ │ │ └── useSelect.ts │ │ │ │ ├── ContentLabel │ │ │ │ │ ├── ContentLabel.type.ts │ │ │ │ │ ├── ContentLabel.tsx │ │ │ │ │ └── ContentLabel.stories.tsx │ │ │ │ ├── Carousel │ │ │ │ │ ├── Carousel.type.ts │ │ │ │ │ ├── CarouselDeleteButton.tsx │ │ │ │ │ ├── CarouselIndicator.tsx │ │ │ │ │ └── CarouselChangeButton.tsx │ │ │ │ ├── TopNav │ │ │ │ │ ├── NavItem.type.ts │ │ │ │ │ ├── NavItem.style.ts │ │ │ │ │ ├── NavItem.tsx │ │ │ │ │ ├── TopNav.style.ts │ │ │ │ │ └── TopNav.tsx │ │ │ │ ├── ContentItem │ │ │ │ │ ├── ContentItem.type.ts │ │ │ │ │ └── ContentItem.style.ts │ │ │ │ ├── Checkbox │ │ │ │ │ └── Checkbox.type.ts │ │ │ │ ├── ChipGroup │ │ │ │ │ ├── ChipGroup.style.ts │ │ │ │ │ ├── ChipGroup.stories.tsx │ │ │ │ │ └── ChipGroup.tsx │ │ │ │ ├── Profile │ │ │ │ │ ├── Profile.type.ts │ │ │ │ │ ├── Profile.tsx │ │ │ │ │ └── Profile.style.ts │ │ │ │ ├── Chevron │ │ │ │ │ ├── Chevron.style.ts │ │ │ │ │ └── Chevron.tsx │ │ │ │ ├── Tabs │ │ │ │ │ ├── Tab.type.ts │ │ │ │ │ ├── useTabContext.ts │ │ │ │ │ └── Tabs.stories.tsx │ │ │ │ ├── IsFixedIcon │ │ │ │ │ ├── IsFixedIcon.style.ts │ │ │ │ │ └── IsFixedIcon.tsx │ │ │ │ ├── BankSelect │ │ │ │ │ ├── BankSpriteInitializer.tsx │ │ │ │ │ ├── BankIcon.tsx │ │ │ │ │ ├── BankSelect.style.ts │ │ │ │ │ └── BankSelect.stories.tsx │ │ │ │ ├── Icons │ │ │ │ │ ├── Icons │ │ │ │ │ │ ├── IconHeundeut.tsx │ │ │ │ │ │ ├── IconX.tsx │ │ │ │ │ │ ├── IconEdit.tsx │ │ │ │ │ │ ├── IconTrash.tsx │ │ │ │ │ │ ├── IconCheck.tsx │ │ │ │ │ │ ├── IconKakao.tsx │ │ │ │ │ │ ├── IconSetting.tsx │ │ │ │ │ │ ├── IconXCircle.tsx │ │ │ │ │ │ ├── IconMeatballs.tsx │ │ │ │ │ │ ├── IconErrorCircle.tsx │ │ │ │ │ │ ├── IconConfirmCircle.tsx │ │ │ │ │ │ ├── IconPictureSquare.tsx │ │ │ │ │ │ ├── IconChevron.tsx │ │ │ │ │ │ └── IconSearch.tsx │ │ │ │ │ ├── Icon.type.ts │ │ │ │ │ └── Svg.style.ts │ │ │ │ ├── ListButton │ │ │ │ │ ├── ListButton.type.ts │ │ │ │ │ ├── ListButton.style.ts │ │ │ │ │ └── ListButton.stories.tsx │ │ │ │ ├── Title │ │ │ │ │ ├── Title.type.ts │ │ │ │ │ ├── Title.stories.tsx │ │ │ │ │ └── Title.style.ts │ │ │ │ ├── DepositCheck │ │ │ │ │ ├── DepositCheck.type.ts │ │ │ │ │ ├── DepositCheck.stories.tsx │ │ │ │ │ └── DepositCheck.style.ts │ │ │ │ ├── Textarea │ │ │ │ │ └── Textarea.type.ts │ │ │ │ ├── Dropdown │ │ │ │ │ ├── useDropdown.ts │ │ │ │ │ └── Dropdown.type.ts │ │ │ │ ├── SendButton │ │ │ │ │ ├── SendButton.style.ts │ │ │ │ │ └── SendButton.stories.tsx │ │ │ │ ├── Container │ │ │ │ │ ├── Container.type.ts │ │ │ │ │ ├── Container.tsx │ │ │ │ │ └── Container.style.ts │ │ │ │ ├── DepositToggle │ │ │ │ │ └── DepositToggle.type.ts │ │ │ │ ├── Image │ │ │ │ │ └── Image.tsx │ │ │ │ ├── ListItem │ │ │ │ │ ├── Row.tsx │ │ │ │ │ ├── ListItem.tsx │ │ │ │ │ └── ListItem.style.ts │ │ │ │ ├── Box │ │ │ │ │ ├── Box.type.ts │ │ │ │ │ └── Box.tsx │ │ │ │ ├── TextButton │ │ │ │ │ ├── TextButton.type.ts │ │ │ │ │ └── TextButton.tsx │ │ │ │ ├── BottomSheet │ │ │ │ │ └── BottomSheet.type.ts │ │ │ │ ├── Input │ │ │ │ │ └── Input.type.ts │ │ │ │ ├── FixedButton │ │ │ │ │ └── FixedButton.type.ts │ │ │ │ ├── Button │ │ │ │ │ └── Button.type.ts │ │ │ │ ├── Chip │ │ │ │ │ ├── Chip.style.ts │ │ │ │ │ └── Chip.tsx │ │ │ │ ├── ExpenseList │ │ │ │ │ └── ExpenseList.type.ts │ │ │ │ ├── Amount │ │ │ │ │ ├── Amount.tsx │ │ │ │ │ └── Amount.stories.tsx │ │ │ │ ├── EditableItem │ │ │ │ │ ├── EditableItem.Input.type.ts │ │ │ │ │ ├── useEditableItem.ts │ │ │ │ │ ├── EditableItem.type.ts │ │ │ │ │ └── EditableItem.style.ts │ │ │ │ ├── ChipButton │ │ │ │ │ └── ChipButton.style.ts │ │ │ │ ├── Lottie │ │ │ │ │ └── Lottie.tsx │ │ │ │ ├── Text │ │ │ │ │ ├── Text.tsx │ │ │ │ │ └── Text.type.ts │ │ │ │ ├── IconButton │ │ │ │ │ ├── IconButton.type.ts │ │ │ │ │ └── IconButton.tsx │ │ │ │ ├── Top │ │ │ │ │ ├── Top.stories.tsx │ │ │ │ │ └── Top.tsx │ │ │ │ ├── Stack │ │ │ │ │ └── Stack.type.ts │ │ │ │ ├── NumberKeyboard │ │ │ │ │ └── keypads.ts │ │ │ │ └── CreatedEventItem │ │ │ │ │ └── CreatedEventItem.style.ts │ │ │ ├── type │ │ │ │ ├── strictPropsWithChildren.ts │ │ │ │ └── withTheme.ts │ │ │ ├── utils │ │ │ │ └── changeCamelCaseToKebabCase.ts │ │ │ ├── theme │ │ │ │ ├── theme.type.ts │ │ │ │ └── commonStyle.ts │ │ │ └── layouts │ │ │ │ ├── FunnelLayout.tsx │ │ │ │ ├── ContentLayout.tsx │ │ │ │ └── MainLayout.tsx │ │ ├── Logo │ │ │ ├── index.ts │ │ │ ├── Logo.style.ts │ │ │ ├── RunningDogLogo.tsx │ │ │ └── StandingDogLogo.tsx │ │ ├── ShareEventButton │ │ │ └── index.ts │ │ ├── Toast │ │ │ ├── ToastContainer.tsx │ │ │ └── Toast.type.ts │ │ ├── AmplitudeInitializer │ │ │ └── AmplitudeInitializer.tsx │ │ ├── Loader │ │ │ ├── UserInfo │ │ │ │ ├── UserInfoProvider.tsx │ │ │ │ └── UserInfoLoader.tsx │ │ │ ├── EventData │ │ │ │ ├── EventDataLoader.tsx │ │ │ │ └── EventDataProvider.tsx │ │ │ └── EventDataProvider.tsx │ │ ├── AmountInput │ │ │ └── AmountInput.stories.tsx │ │ ├── Modal │ │ │ └── BankSelectModal │ │ │ │ └── BankSelectModal.style.ts │ │ ├── StepList │ │ │ └── Steps.tsx │ │ └── Reports │ │ │ └── Reports.tsx │ ├── mocks │ │ ├── svg.ts │ │ ├── serverConstants.ts │ │ ├── imageFileMock.ts │ │ ├── server.ts │ │ ├── browser.ts │ │ ├── mockEndpointPrefix.ts │ │ ├── validValueForTest.ts │ │ ├── handlers │ │ │ ├── reportHandlers.ts │ │ │ └── testHandlers.ts │ │ └── handlers.ts │ ├── hooks │ │ ├── useSearchReports │ │ │ ├── index.ts │ │ │ └── useSearchReports.tsx │ │ ├── useUserInfoContext.ts │ │ ├── useEventDataContext.tsx │ │ ├── queries │ │ │ ├── auth │ │ │ │ ├── useRequestGetKakaoClientId.ts │ │ │ │ └── useRequestGetKakaoLogin.ts │ │ │ ├── images │ │ │ │ ├── useRequestGetImages.ts │ │ │ │ ├── useRequestDeleteImages.ts │ │ │ │ └── useRequestPostImages.ts │ │ │ ├── event │ │ │ │ ├── useRequestPostUserEvent.ts │ │ │ │ ├── useRequestPatchUser.ts │ │ │ │ ├── useRequestDeleteEvents.ts │ │ │ │ └── useRequestPostGuestEvent.ts │ │ │ ├── member │ │ │ │ ├── useRequestGetAllMembers.ts │ │ │ │ └── useRequestGetCurrentMembers.ts │ │ │ ├── step │ │ │ │ └── useRequestGetSteps.ts │ │ │ └── user │ │ │ │ └── useRequestDeleteUser.ts │ │ ├── useToast │ │ │ └── toastEventManager.type.ts │ │ ├── usePageBackground.ts │ │ ├── createEvent │ │ │ └── useCreateGuestEventData.tsx │ │ ├── useWithdrawFunnel.ts │ │ └── usePriceStep.ts │ ├── apis │ │ ├── baseUrl.ts │ │ ├── endpointPrefix.ts │ │ ├── withId.type.ts │ │ └── request │ │ │ ├── report.ts │ │ │ └── step.ts │ ├── utils │ │ ├── isDuplicate.ts │ │ ├── validate │ │ │ ├── type.ts │ │ │ ├── validateEventName.ts │ │ │ └── validateEventPassword.ts │ │ ├── getKakaoRedirectUrl.ts │ │ ├── getImageUrl.ts │ │ ├── isRequestError.ts │ │ ├── getEventBaseUrl.ts │ │ ├── caculateExpense.ts │ │ ├── objectToQueryString.ts │ │ ├── getEventIdByUrl.ts │ │ ├── udpateMetaTag.ts │ │ ├── getEventPageUrlByEnvironment.ts │ │ ├── isArraysEqual.ts │ │ ├── SessionStorage.ts │ │ ├── detectDevice.ts │ │ └── detectBrowser.ts │ ├── assets │ │ └── image │ │ │ ├── x.svg │ │ │ ├── chevron.svg │ │ │ ├── check.svg │ │ │ ├── kakao.svg │ │ │ ├── edit.svg │ │ │ └── error-circle.svg │ ├── errors │ │ ├── requestErrorType.ts │ │ ├── RequestError.ts │ │ └── RequestGetError.ts │ ├── store │ │ ├── appErrorStore.ts │ │ ├── stepsStore.ts │ │ ├── authStore.ts │ │ └── totalExpenseAmountStore.ts │ ├── UnPredictableErrorBoundary.tsx │ ├── global.d.ts │ └── types │ │ └── toastType.ts ├── public │ └── favicon.ico ├── .gitignore ├── .prettierrc ├── cypress.config.ts ├── jest.polyfills.ts └── jest.setup.ts ├── .gitmodules ├── .github ├── pull-request-template.md ├── ISSUE_TEMPLATE │ ├── feature-template.md │ └── bug-template.md └── auto_assign.yml ├── .idea └── modules │ └── haengdong.main.iml └── README.md /server/docs/migration.sql: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | import './commands'; 2 | -------------------------------------------------------------------------------- /server/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'haengdong' 2 | -------------------------------------------------------------------------------- /client/.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict = true 2 | legacy-peer-deps = true 3 | -------------------------------------------------------------------------------- /client/src/constants/password.ts: -------------------------------------------------------------------------------- 1 | export const PASSWORD_LENGTH = 4; 2 | -------------------------------------------------------------------------------- /client/src/pages/landing/Nav/index.ts: -------------------------------------------------------------------------------- 1 | export {default as Nav} from './Nav'; 2 | -------------------------------------------------------------------------------- /client/src/components/Footer/index.ts: -------------------------------------------------------------------------------- 1 | export {default as Footer} from './Footer'; 2 | -------------------------------------------------------------------------------- /client/src/pages/landing/index.ts: -------------------------------------------------------------------------------- 1 | export {default as LandingPage} from './LandingPage'; 2 | -------------------------------------------------------------------------------- /client/src/mocks/svg.ts: -------------------------------------------------------------------------------- 1 | export default 'SvgrURL'; 2 | export const ReactComponent = 'div'; 3 | -------------------------------------------------------------------------------- /client/src/pages/event/[eventId]/home/index.ts: -------------------------------------------------------------------------------- 1 | export {default as HomePage} from './HomePage'; 2 | -------------------------------------------------------------------------------- /client/cypress/fixtures/postEvent.json: -------------------------------------------------------------------------------- 1 | { 2 | "eventId": "550e8400-e29b-41d4-a716-446655440000" 3 | } -------------------------------------------------------------------------------- /client/src/components/Design/components/Banner/index.ts: -------------------------------------------------------------------------------- 1 | export {default as Banner} from './Banner'; 2 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Select/index.ts: -------------------------------------------------------------------------------- 1 | export {default as Select} from './Select'; 2 | -------------------------------------------------------------------------------- /client/src/pages/event/[eventId]/admin/index.ts: -------------------------------------------------------------------------------- 1 | export {default as AdminPage} from './AdminPage'; 2 | -------------------------------------------------------------------------------- /client/src/pages/event/[eventId]/index.ts: -------------------------------------------------------------------------------- 1 | export {default as EventPage} from './EventPageLayout'; 2 | -------------------------------------------------------------------------------- /client/src/hooks/useSearchReports/index.ts: -------------------------------------------------------------------------------- 1 | export {default as useSearchReports} from './useSearchReports'; 2 | -------------------------------------------------------------------------------- /client/src/pages/landing/Section/MainSection/index.ts: -------------------------------------------------------------------------------- 1 | export {default as MainSection} from './MainSection'; 2 | -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse-teams/2024-haeng-dong/HEAD/client/public/favicon.ico -------------------------------------------------------------------------------- /client/src/pages/landing/Section/FeatureSection/index.ts: -------------------------------------------------------------------------------- 1 | export {default as FeatureSection} from './FeatureSection'; 2 | -------------------------------------------------------------------------------- /client/src/mocks/serverConstants.ts: -------------------------------------------------------------------------------- 1 | export const VALID_EVENT_NAME_LENGTH_IN_SERVER = { 2 | min: 2, 3 | max: 30, 4 | }; 5 | -------------------------------------------------------------------------------- /client/src/pages/landing/Section/FeatureSection/SimpleShare/index.ts: -------------------------------------------------------------------------------- 1 | export {default as SimpleShare} from './SimpleShare'; 2 | -------------------------------------------------------------------------------- /client/src/apis/baseUrl.ts: -------------------------------------------------------------------------------- 1 | export const BASE_URL = { 2 | HD: process.env.API_BASE_URL, 3 | S3: process.env.S3_URL, 4 | }; 5 | -------------------------------------------------------------------------------- /client/src/pages/landing/Section/DescriptionSection/index.ts: -------------------------------------------------------------------------------- 1 | export {default as DescriptionSection} from './DescriptionSection'; 2 | -------------------------------------------------------------------------------- /client/src/pages/landing/Section/FeatureSection/AutoCalculate/index.ts: -------------------------------------------------------------------------------- 1 | export {default as AutoCalculate} from './AutoCalculate'; 2 | -------------------------------------------------------------------------------- /client/src/pages/landing/Section/FeatureSection/CheckDeposit/index.ts: -------------------------------------------------------------------------------- 1 | export {default as CheckDeposit} from './CheckDeposit'; 2 | -------------------------------------------------------------------------------- /client/src/pages/landing/Section/FeatureSection/SimpleTransfer/index.ts: -------------------------------------------------------------------------------- 1 | export {default as SimpleTransfer} from './SimpleTransfer'; 2 | -------------------------------------------------------------------------------- /client/src/constants/message.ts: -------------------------------------------------------------------------------- 1 | const MESSAGE = { 2 | confirmEditEventMember: '수정이 완료되었어요 :)', 3 | }; 4 | 5 | export default MESSAGE; 6 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/user/domain/Role.java: -------------------------------------------------------------------------------- 1 | package haengdong.user.domain; 2 | 3 | public enum Role { 4 | GUEST, MEMBER 5 | } 6 | -------------------------------------------------------------------------------- /server/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/woowacourse-teams/2024-haeng-dong/HEAD/server/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /client/src/components/Logo/index.ts: -------------------------------------------------------------------------------- 1 | export {default as StandingDog} from './StandingDogLogo'; 2 | export {default as RunningDog} from './RunningDogLogo'; 3 | -------------------------------------------------------------------------------- /client/src/pages/landing/Section/FeatureSection/RecordMemoryWithPhoto/index.ts: -------------------------------------------------------------------------------- 1 | export {default as RecordMemoryWithPhoto} from './RecordMemoryWithPhoto'; 2 | -------------------------------------------------------------------------------- /client/src/components/Design/type/strictPropsWithChildren.ts: -------------------------------------------------------------------------------- 1 | export type StrictPropsWithChildren

= P & { 2 | children: React.ReactNode; 3 | }; 4 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/user/application/UserDeleteEvent.java: -------------------------------------------------------------------------------- 1 | package haengdong.user.application; 2 | 3 | public record UserDeleteEvent(Long id) { 4 | } 5 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ContentLabel/ContentLabel.type.ts: -------------------------------------------------------------------------------- 1 | export type ContentLabelProps = React.PropsWithChildren & { 2 | onClick?: () => void; 3 | }; 4 | -------------------------------------------------------------------------------- /client/src/components/Design/type/withTheme.ts: -------------------------------------------------------------------------------- 1 | import {Theme} from '@theme/theme.type'; 2 | 3 | export type WithTheme

= P & { 4 | theme: Theme; 5 | }; 6 | -------------------------------------------------------------------------------- /client/src/mocks/imageFileMock.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | process() { 3 | return { 4 | code: 'module.exports = "imageFileMock";', 5 | }; 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /client/src/mocks/server.ts: -------------------------------------------------------------------------------- 1 | import {setupServer} from 'msw/node'; 2 | 3 | import {handlers} from './handlers'; 4 | 5 | export const server = setupServer(...handlers); 6 | -------------------------------------------------------------------------------- /client/src/utils/isDuplicate.ts: -------------------------------------------------------------------------------- 1 | const isDuplicated = (arr: string[], target: string) => { 2 | return arr.includes(target); 3 | }; 4 | 5 | export default isDuplicated; 6 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "server/src/main/resources/config"] 2 | path = server/src/main/resources/config 3 | url = https://github.com/woowacourse-teams/2024-haeng-dong-config 4 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Carousel/Carousel.type.ts: -------------------------------------------------------------------------------- 1 | export interface CarouselProps { 2 | urls: string[]; 3 | onClickDelete?: (index: number) => void; 4 | } 5 | -------------------------------------------------------------------------------- /client/src/mocks/browser.ts: -------------------------------------------------------------------------------- 1 | import {setupWorker} from 'msw/browser'; 2 | 3 | import {handlers} from './handlers'; 4 | 5 | export const worker = setupWorker(...handlers); 6 | -------------------------------------------------------------------------------- /client/src/mocks/mockEndpointPrefix.ts: -------------------------------------------------------------------------------- 1 | import {BASE_URL} from '@apis/baseUrl'; 2 | 3 | export const MOCK_API_PREFIX = typeof window !== 'undefined' ? `${BASE_URL.HD}` : ''; 4 | -------------------------------------------------------------------------------- /client/cypress/constants/constants.ts: -------------------------------------------------------------------------------- 1 | const CONSTANTS = { 2 | eventName: '테스트 이벤트', 3 | eventPassword: '1234', 4 | adminName: '쿠키', 5 | }; 6 | 7 | export default CONSTANTS; 8 | -------------------------------------------------------------------------------- /client/src/utils/validate/type.ts: -------------------------------------------------------------------------------- 1 | export interface ValidateResult { 2 | isValid: boolean; 3 | errorMessage: string | null; 4 | errorInfo?: Record; 5 | } 6 | -------------------------------------------------------------------------------- /client/src/apis/endpointPrefix.ts: -------------------------------------------------------------------------------- 1 | export const MEMBER_API_PREFIX = '/api/events'; 2 | export const ADMIN_API_PREFIX = '/api/admin/events'; 3 | export const USER_API_PREFIX = '/api/users'; 4 | -------------------------------------------------------------------------------- /client/src/apis/withId.type.ts: -------------------------------------------------------------------------------- 1 | export type WithEventId

= P & { 2 | eventId: string; 3 | }; 4 | 5 | export type WithBillId

= P & { 6 | billId: number; 7 | }; 8 | -------------------------------------------------------------------------------- /client/src/components/Design/components/TopNav/NavItem.type.ts: -------------------------------------------------------------------------------- 1 | import {PropsWithChildren} from 'react'; 2 | 3 | export type NavItemProps = PropsWithChildren & { 4 | routePath?: string; 5 | }; 6 | -------------------------------------------------------------------------------- /client/src/pages/event/create/guest/index.ts: -------------------------------------------------------------------------------- 1 | export {default as SetGuestEventNameStep} from './SetGuestEventNameStep'; 2 | export {default as SetEventPasswordStep} from './SetEventPasswordStep'; 3 | -------------------------------------------------------------------------------- /client/src/utils/getKakaoRedirectUrl.ts: -------------------------------------------------------------------------------- 1 | const getKakaoRedirectUrl = () => { 2 | return window.location.origin + process.env.KAKAO_REDIRECT_URI; 3 | }; 4 | 5 | export default getKakaoRedirectUrl; 6 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/EventAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | public record EventAppRequest(String name, Long userId) { 4 | } 5 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ContentItem/ContentItem.type.ts: -------------------------------------------------------------------------------- 1 | export type ContentItemProps = React.PropsWithChildren & { 2 | labels?: React.ReactElement; 3 | onEditClick?: () => void; 4 | }; 5 | -------------------------------------------------------------------------------- /client/src/components/Design/utils/changeCamelCaseToKebabCase.ts: -------------------------------------------------------------------------------- 1 | export const changeCamelCaseToKebabCase = (str: string) => { 2 | return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 3 | }; 4 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/user/presentation/response/KakaoClientId.java: -------------------------------------------------------------------------------- 1 | package haengdong.user.presentation.response; 2 | 3 | public record KakaoClientId( 4 | String clientId 5 | ) { 6 | } 7 | -------------------------------------------------------------------------------- /client/src/components/ShareEventButton/index.ts: -------------------------------------------------------------------------------- 1 | export {default as DesktopShareEventButton} from './DesktopShareEventButton'; 2 | export {default as MobileShareEventButton} from './MobileShareEventButton'; 3 | -------------------------------------------------------------------------------- /client/src/utils/getImageUrl.ts: -------------------------------------------------------------------------------- 1 | const getImageUrl = (name: string, format: 'png' | 'webp' | 'svg') => { 2 | return `${process.env.IMAGE_URL}/${name}.${format}`; 3 | }; 4 | 5 | export default getImageUrl; 6 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/EventLoginAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | public record EventLoginAppRequest(String token, String password) { 4 | } 5 | -------------------------------------------------------------------------------- /client/src/utils/isRequestError.ts: -------------------------------------------------------------------------------- 1 | import RequestError from '@errors/RequestError'; 2 | 3 | const isRequestError = (error: Error) => { 4 | return error instanceof RequestError; 5 | }; 6 | 7 | export default isRequestError; 8 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/domain/RandomValueProvider.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.domain; 2 | 3 | import java.util.UUID; 4 | 5 | public interface RandomValueProvider { 6 | 7 | String createRandomValue(); 8 | } 9 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/request/EventDeleteRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.request; 2 | 3 | import java.util.List; 4 | 5 | public record EventDeleteRequest(List eventIds) { 6 | } 7 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/BillUpdateAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | public record BillUpdateAppRequest( 4 | String title, 5 | Long price 6 | ) { 7 | } 8 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/EventImageAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | public record EventImageAppResponse( 4 | Long id, 5 | String name 6 | ) { 7 | } 8 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Banner/Banner.type.ts: -------------------------------------------------------------------------------- 1 | export type BannerProps = React.HTMLAttributes & { 2 | onDelete: () => void; 3 | title: string; 4 | description?: string; 5 | buttonText?: string; 6 | }; 7 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/MemberNameUpdateAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | public record MemberNameUpdateAppRequest( 4 | Long id, 5 | String name 6 | ) { 7 | } 8 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/MemberSaveAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | import haengdong.user.domain.Nickname; 4 | 5 | public record MemberSaveAppRequest(Nickname name) { 6 | } 7 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/EventImageUrlAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | public record EventImageUrlAppResponse( 4 | Long id, 5 | String url 6 | ) { 7 | } 8 | -------------------------------------------------------------------------------- /client/src/utils/getEventBaseUrl.ts: -------------------------------------------------------------------------------- 1 | const getEventBaseUrl = (url: string) => { 2 | const urlParts = url.split('/'); 3 | const baseUrl = urlParts[1] + '/' + urlParts[2]; 4 | 5 | return baseUrl; 6 | }; 7 | 8 | export default getEventBaseUrl; 9 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/EventDeleteAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | import java.util.List; 4 | 5 | public record EventDeleteAppRequest(Long token, List eventIds) { 6 | } 7 | -------------------------------------------------------------------------------- /client/src/assets/image/x.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Checkbox/Checkbox.type.ts: -------------------------------------------------------------------------------- 1 | import {InputHTMLAttributes, ReactNode} from 'react'; 2 | 3 | export interface CheckboxProps extends Omit, 'type'> { 4 | right?: ReactNode; 5 | } 6 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ChipGroup/ChipGroup.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const chipGroupStyle = css({ 4 | display: 'flex', 5 | gap: '0.25rem', 6 | flexWrap: 'wrap', 7 | overflow: 'hidden', 8 | }); 9 | -------------------------------------------------------------------------------- /client/src/components/Design/components/TopNav/NavItem.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const navItemStyle = css({ 4 | padding: '0 0.5rem', 5 | 6 | ':first-of-type': { 7 | paddingLeft: 0, 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /client/src/pages/event/[eventId]/qrcode/QRCodePage.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const QRCodeStyle = () => css` 4 | position: relative; 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | `; 9 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | *.log 3 | npm-debug.log* 4 | 5 | node_modules 6 | dist 7 | 8 | .env.* 9 | 10 | *storybook.log 11 | .DS_Store 12 | 13 | # Sentry Config File 14 | .env.sentry-build-plugin 15 | 16 | storybook-static 17 | *storybook.log 18 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Profile/Profile.type.ts: -------------------------------------------------------------------------------- 1 | import {ImageProps} from '../Image/Image'; 2 | 3 | export type ProfileSize = 'small' | 'medium' | 'large'; 4 | 5 | export type ProfileProps = ImageProps & { 6 | size?: ProfileSize; 7 | }; 8 | -------------------------------------------------------------------------------- /client/src/mocks/validValueForTest.ts: -------------------------------------------------------------------------------- 1 | export const VALID_PASSWORD_FOR_TEST = 1111; 2 | export const VALID_TOKEN_FOR_TEST = 'valid-token'; 3 | export const FORBIDDEN_TOKEN_FOR_TEST = 'forbidden-token'; 4 | export const EXPIRED_TOKEN_FOR_TEST = 'expired-token'; 5 | -------------------------------------------------------------------------------- /client/src/assets/image/chevron.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/ImageInfo.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import java.time.Instant; 4 | 5 | public record ImageInfo( 6 | String name, 7 | Instant createAt 8 | ) { 9 | } 10 | -------------------------------------------------------------------------------- /.github/pull-request-template.md: -------------------------------------------------------------------------------- 1 | ## issue 2 | - close #n 3 | 4 | ## 구현 사항 5 | 어떤 것을 구현했는지 필요히다면 사진 || 영상과 함께 자세히 설명해주세요. 6 | 7 | ## 중점적으로 리뷰받고 싶은 부분(선택) 8 | 어떤 부분을 중점으로 리뷰했으면 좋겠는지 작성해주세요. 9 | 10 | ## 논의하고 싶은 부분(선택) 11 | 논의하고 싶은 부분이 있다면 작성해주세요. 12 | 13 | ## 🫡 참고사항 14 | -------------------------------------------------------------------------------- /client/src/assets/image/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/src/pages/event/[eventId]/home/HomePage.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const receiptStyle = css({ 4 | display: 'flex', 5 | flexDirection: 'column', 6 | gap: '0.5rem', 7 | paddingInline: '1rem', 8 | paddingBottom: '2rem', 9 | }); 10 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/MemberNamesUpdateAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | import java.util.List; 4 | 5 | public record MemberNamesUpdateAppRequest( 6 | List members 7 | ) { 8 | } 9 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Chevron/Chevron.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const chevronStyle = css({ 4 | transition: 'transform 0.3s ease', 5 | }); 6 | 7 | export const activeChevronStyle = css({ 8 | transform: 'rotate(180deg)', 9 | }); 10 | -------------------------------------------------------------------------------- /client/src/components/Footer/Footer.type.ts: -------------------------------------------------------------------------------- 1 | export interface FooterStyleProps {} 2 | 3 | export interface FooterCustomProps {} 4 | 5 | export type FooterOptionProps = FooterStyleProps & FooterCustomProps; 6 | 7 | export type FooterProps = React.ComponentProps<'footer'> & FooterOptionProps; 8 | -------------------------------------------------------------------------------- /client/src/errors/requestErrorType.ts: -------------------------------------------------------------------------------- 1 | import {Body, Method} from '@apis/request'; 2 | 3 | export type RequestErrorType = Error & { 4 | requestBody: Body; 5 | status: number; 6 | endpoint: string; 7 | errorCode: string; 8 | message: string; 9 | method?: Method; 10 | }; 11 | -------------------------------------------------------------------------------- /client/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 120, 5 | "tabWidth": 2, 6 | "semi": true, 7 | "arrowParens": "avoid", 8 | "endOfLine": "auto", 9 | "jsxSingleQuote": false, 10 | "bracketSpacing": false, 11 | "proseWrap": "preserve" 12 | } -------------------------------------------------------------------------------- /client/src/components/Design/components/TopNav/NavItem.tsx: -------------------------------------------------------------------------------- 1 | import {navItemStyle} from './NavItem.style'; 2 | import {NavItemProps} from './NavItem.type'; 3 | 4 | const NavItem = ({children}: NavItemProps) => { 5 | return

  • {children}
  • ; 6 | }; 7 | 8 | export default NavItem; 9 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Tabs/Tab.type.ts: -------------------------------------------------------------------------------- 1 | export type TabProps = Omit, 'content'> & { 2 | label: string; 3 | content: React.ReactNode; 4 | index?: number; 5 | }; 6 | 7 | export type TabsProps = { 8 | children: React.ReactElement[]; 9 | }; 10 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ContentItem/ContentItem.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const containerStyle = css({ 4 | position: 'relative', 5 | width: '100%', 6 | }); 7 | 8 | export const iconStyle = css({ 9 | position: 'absolute', 10 | right: '1rem', 11 | }); 12 | -------------------------------------------------------------------------------- /client/src/components/Logo/Logo.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const logoStyle = css({ 4 | display: 'flex', 5 | justifyContent: 'center', 6 | 7 | width: '100%', 8 | }); 9 | 10 | export const logoImageStyle = css({ 11 | maxWidth: '300px', 12 | width: '100%', 13 | }); 14 | -------------------------------------------------------------------------------- /client/src/constants/regExp.ts: -------------------------------------------------------------------------------- 1 | const REGEXP = { 2 | eventPassword: /^[0-9]*$/, 3 | eventUrl: /\/event\/([a-zA-Z0-9-]+)\//, 4 | billTitle: /^([ㄱ-ㅎ가-힣a-zA-Z0-9ㆍᆢ]\s?)*$/, 5 | memberName: /^([ㄱ-ㅎ가-힣a-zA-Zㆍᆢ]\s?)*$/, 6 | accountNumber: /^\d+([\s\-]\d+)*[\s\-]?$/, 7 | }; 8 | 9 | export default REGEXP; 10 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:17-jdk-slim 2 | 3 | WORKDIR /app 4 | 5 | COPY /build/libs/*.jar /app/haengdong-0.0.1-SNAPSHOT.jar 6 | 7 | EXPOSE 8080 8 | ENTRYPOINT ["java"] 9 | CMD ["-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE}", "-Duser.timezone=Asia/Seoul", "-jar", "haengdong-0.0.1-SNAPSHOT.jar"] 10 | -------------------------------------------------------------------------------- /client/src/pages/login/LoginPage.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | import {Theme} from '@components/Design/theme/theme.type'; 4 | 5 | export const hrStyle = (theme: Theme) => 6 | css({ 7 | width: '100%', 8 | height: 1, 9 | 10 | backgroundColor: theme.colors.tertiary, 11 | }); 12 | -------------------------------------------------------------------------------- /client/src/utils/caculateExpense.ts: -------------------------------------------------------------------------------- 1 | import {Step} from 'types/serviceType'; 2 | 3 | export const getTotalExpenseAmount = (steps: Step[]) => { 4 | return steps.reduce((total, step) => { 5 | const stepTotal = step.bills.reduce((sum, bill) => sum + bill.price, 0); 6 | return total + stepTotal; 7 | }, 0); 8 | }; 9 | -------------------------------------------------------------------------------- /server/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/common/properties/JwtProperties.java: -------------------------------------------------------------------------------- 1 | package haengdong.common.properties; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | @ConfigurationProperties("security.jwt.token") 6 | public record JwtProperties(String secretKey, Long expireLength) { 7 | } 8 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Template 3 | about: 기능에 관한 템플릿 4 | title: "" 5 | labels: ⚙️ feat 6 | assignees: '' 7 | --- 8 | 9 | ## 📄 설명 10 | 11 | 추가하려는 기능에 대해 간결하게 설명해주세요. 12 | 13 | ## 🏁 할 일 14 | 15 | - [ ] 예상되는 작업을 상세하게 작성해주세요. (checkBox 형태로 작성해주세요.) 16 | 17 | ## 🫡 참고사항 18 | -------------------------------------------------------------------------------- /client/src/components/Design/components/IsFixedIcon/IsFixedIcon.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | import {WithTheme} from '@type/withTheme'; 4 | 5 | export const isFixedIconStyle = ({theme}: WithTheme) => 6 | css({ 7 | color: theme.colors.error, 8 | paddingRight: '0.25rem', 9 | }); 10 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/common/config/JpaConfig.java: -------------------------------------------------------------------------------- 1 | package haengdong.common.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.data.jpa.repository.config.EnableJpaAuditing; 5 | 6 | @Configuration 7 | @EnableJpaAuditing 8 | public class JpaConfig { 9 | } 10 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/request/EventSaveRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | 5 | public record EventSaveRequest( 6 | @NotBlank(message = "행사 이름은 공백일 수 없습니다.") 7 | String eventName 8 | ) { 9 | } 10 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/request/MemberSaveRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | 5 | public record MemberSaveRequest( 6 | 7 | @NotBlank(message = "참여자 이름은 공백일 수 없습니다.") 8 | String name 9 | ) { 10 | } 11 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | # Reviewer 자동 할당 설정 2 | addReviewers: true 3 | 4 | # Assignee를 Author로 설정 5 | addAssignees: author 6 | 7 | # Reviewer 추가할 사용자 목록 8 | reviewers: 9 | - jinhokim98 10 | - pakxe 11 | - soi-ha 12 | - Todari 13 | 14 | # 추가할 리뷰어 수 15 | # 0으로 설정하면 그룹 내 모든 리뷰어를 추가합니다 (기본값: 0) 16 | numberOfReviewers: 0 17 | -------------------------------------------------------------------------------- /client/src/components/Design/theme/theme.type.ts: -------------------------------------------------------------------------------- 1 | import {ColorTokens} from '@token/colors'; 2 | import {TypographyTokens} from '@token/typography'; 3 | import {ZIndexTokens} from '@token/zIndex'; 4 | 5 | export interface Theme { 6 | colors: ColorTokens; 7 | typography: TypographyTokens; 8 | zIndex: ZIndexTokens; 9 | } 10 | -------------------------------------------------------------------------------- /client/src/components/Design/components/BankSelect/BankSpriteInitializer.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {createPortal} from 'react-dom'; 3 | 4 | import BankSprite from '@assets/image/largeBankSprites.svg'; 5 | 6 | export const BankSpriteInitializer = () => { 7 | return createPortal(, document.body); 8 | }; 9 | -------------------------------------------------------------------------------- /client/cypress.config.ts: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'cypress'; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | baseUrl: 'http://localhost:3000', 6 | viewportWidth: 430, 7 | viewportHeight: 930, 8 | // setupNodeEvents(on, config) { 9 | // // implement node event listeners here 10 | // }, 11 | }, 12 | }); 13 | -------------------------------------------------------------------------------- /client/src/components/Design/theme/commonStyle.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const srOnlyStyle = css` 4 | position: absolute; 5 | width: 1px; 6 | height: 1px; 7 | padding: 0; 8 | margin: -1px; 9 | overflow: hidden; 10 | clip: rect(0, 0, 0, 0); 11 | white-space: nowrap; 12 | border-width: 0; 13 | `; 14 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/common/properties/CorsProperties.java: -------------------------------------------------------------------------------- 1 | package haengdong.common.properties; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | @ConfigurationProperties("cors") 6 | public record CorsProperties( 7 | Long maxAge, 8 | String[] allowedOrigins 9 | ) { 10 | } 11 | -------------------------------------------------------------------------------- /client/src/pages/setting/SettingPage.type.ts: -------------------------------------------------------------------------------- 1 | import {useNavigate} from 'react-router-dom'; 2 | 3 | export type Tab = {name: string; onClick: () => void}; 4 | 5 | export interface TabActions { 6 | navigate: ReturnType; 7 | } 8 | 9 | export interface CategoryProps { 10 | categoryTitle: string; 11 | tabList: Tab[]; 12 | } 13 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/common/auth/TokenProvider.java: -------------------------------------------------------------------------------- 1 | package haengdong.common.auth; 2 | 3 | import java.util.Map; 4 | 5 | public interface TokenProvider { 6 | 7 | String createToken(Map payload); 8 | 9 | Map getPayload(String token); 10 | 11 | boolean validateToken(String token); 12 | } 13 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconHeundeut.tsx: -------------------------------------------------------------------------------- 1 | import getImageUrl from '@utils/getImageUrl'; 2 | 3 | import Image from '../../Image/Image'; 4 | 5 | export const IconHeundeut = () => { 6 | return ( 7 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /client/src/utils/objectToQueryString.ts: -------------------------------------------------------------------------------- 1 | import {ObjectQueryParams} from '@apis/request'; 2 | 3 | const objectToQueryString = (params: ObjectQueryParams): string => { 4 | return Object.entries(params) 5 | .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`) 6 | .join('&'); 7 | }; 8 | 9 | export default objectToQueryString; 10 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/MemberBillReportAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import haengdong.user.domain.Nickname; 4 | 5 | public record MemberBillReportAppResponse( 6 | Long memberId, 7 | Nickname name, 8 | boolean isDeposited, 9 | Long price 10 | ) { 11 | } 12 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/properties/ImageProperties.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.properties; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | @ConfigurationProperties("image") 6 | public record ImageProperties( 7 | String bucket, 8 | String directory, 9 | String baseUrl 10 | ) { 11 | } 12 | -------------------------------------------------------------------------------- /client/src/components/Design/components/TopNav/TopNav.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const topNavStyle = css({ 4 | display: 'flex', 5 | justifyContent: 'space-between', 6 | alignItems: 'center', 7 | width: '100%', 8 | }); 9 | 10 | export const topNavWrapperStyle = css({ 11 | display: 'flex', 12 | alignItems: 'center', 13 | }); 14 | -------------------------------------------------------------------------------- /client/src/store/appErrorStore.ts: -------------------------------------------------------------------------------- 1 | import {create} from 'zustand'; 2 | 3 | type State = { 4 | appError: Error | null; 5 | }; 6 | 7 | type Action = { 8 | updateAppError: (appError: State['appError']) => void; 9 | }; 10 | 11 | export const useAppErrorStore = create(set => ({ 12 | appError: null, 13 | updateAppError: appError => set(() => ({appError})), 14 | })); 15 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/user/application/response/KakaoTokenResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.user.application.response; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | 5 | public record KakaoTokenResponse( 6 | @JsonProperty("access_token") 7 | String accessToken, 8 | 9 | @JsonProperty("id_token") 10 | String idToken 11 | ) { 12 | } 13 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ListButton/ListButton.type.ts: -------------------------------------------------------------------------------- 1 | export interface ListButtonStyleProps {} 2 | 3 | export interface ListButtonCustomProps { 4 | prefix?: string; 5 | suffix?: string; 6 | } 7 | 8 | export type ListButtonOptionProps = ListButtonStyleProps & ListButtonCustomProps; 9 | 10 | export type ListButtonProps = React.ComponentProps<'button'> & ListButtonOptionProps; 11 | -------------------------------------------------------------------------------- /client/src/components/Toast/ToastContainer.tsx: -------------------------------------------------------------------------------- 1 | import {useToast} from '@hooks/useToast/useToast'; 2 | 3 | import Toast from './Toast'; 4 | 5 | const ToastContainer = () => { 6 | const {currentToast, closeToast} = useToast(); 7 | 8 | return <>{currentToast && }; 9 | }; 10 | 11 | export default ToastContainer; 12 | -------------------------------------------------------------------------------- /client/src/store/stepsStore.ts: -------------------------------------------------------------------------------- 1 | import {create} from 'zustand'; 2 | 3 | import {Steps} from 'types/serviceType'; 4 | 5 | type State = { 6 | steps: Steps[]; 7 | }; 8 | 9 | type Action = { 10 | updateSteps: (stepList: State['steps']) => void; 11 | }; 12 | 13 | export const useBillsStore = create(set => ({ 14 | steps: [], 15 | updateSteps: steps => set(() => ({steps})), 16 | })); 17 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/user/domain/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package haengdong.user.domain.repository; 2 | 3 | import java.util.Optional; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import haengdong.user.domain.User; 6 | 7 | public interface UserRepository extends JpaRepository { 8 | 9 | Optional findByMemberNumber(String memberNumber); 10 | } 11 | -------------------------------------------------------------------------------- /client/src/components/Design/components/IsFixedIcon/IsFixedIcon.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | 3 | import {useTheme} from '@theme/HDesignProvider'; 4 | 5 | import {isFixedIconStyle} from './IsFixedIcon.style'; 6 | 7 | const IsFixedIcon = () => { 8 | const {theme} = useTheme(); 9 | 10 | return
    *
    ; 11 | }; 12 | 13 | export default IsFixedIcon; 14 | -------------------------------------------------------------------------------- /client/src/utils/getEventIdByUrl.ts: -------------------------------------------------------------------------------- 1 | import REGEXP from '@constants/regExp'; 2 | 3 | const extractEventIdFromUrl = (url: string) => { 4 | const regex = REGEXP.eventUrl; 5 | const match = url.match(regex); 6 | return match ? match[1] : null; 7 | }; 8 | 9 | const getEventIdByUrl = () => { 10 | return extractEventIdFromUrl(window.location.pathname) ?? ''; 11 | }; 12 | 13 | export default getEventIdByUrl; 14 | -------------------------------------------------------------------------------- /client/src/utils/udpateMetaTag.ts: -------------------------------------------------------------------------------- 1 | export const updateMetaTag = (name: string, content: string) => { 2 | let metaTag = document.querySelector(`meta[property="${name}"]`); 3 | 4 | if (!metaTag) { 5 | metaTag = document.createElement('meta'); 6 | metaTag.setAttribute('property', name); 7 | document.head.appendChild(metaTag); 8 | } 9 | 10 | metaTag.setAttribute('content', content); 11 | }; 12 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/EventResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import haengdong.event.application.response.EventAppResponse; 4 | 5 | public record EventResponse(String eventId) { 6 | 7 | public static EventResponse of(EventAppResponse eventAppResponse) { 8 | return new EventResponse(eventAppResponse.token()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Title/Title.type.ts: -------------------------------------------------------------------------------- 1 | export interface TitleStyleProps {} 2 | 3 | export interface TitleCustomProps { 4 | title: string; 5 | amount?: number; 6 | icon?: React.ReactNode; 7 | dropdown?: React.ReactNode; 8 | } 9 | 10 | export type TitleOptionProps = TitleStyleProps & TitleCustomProps; 11 | 12 | export type TitleProps = React.ComponentProps<'div'> & TitleOptionProps; 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Template 3 | about: 버그를 제보하는 템플릿 4 | title: "" 5 | labels: 🚨bug 6 | assignees: '' 7 | --- 8 | 9 | ## 📄 버그 내용 10 | 어떤 버그인지 간결하게 설명해주세요. 11 | 12 | ## 🚨 버그 발생 상황 13 | 최대한 상세하게 작성해주세요. 14 | 15 | ### as-is 16 | 17 | 현재 상황에 대해서 알려주세요. 18 | 19 | ### to-be 20 | 21 | 구현이 된 후 상황을 예상해 주세요. 22 | 23 | ## 예상 결과 24 | 예상했던 정상적인 결과가 어떤 것인지 설명해주세요. 25 | 26 | ## 🫡 참고사항 27 | -------------------------------------------------------------------------------- /client/src/constants/rule.ts: -------------------------------------------------------------------------------- 1 | const EVENT_PASSWORD_LENGTH = 4; 2 | 3 | const RULE = { 4 | maxEventNameLength: 20, 5 | maxEventPasswordLength: EVENT_PASSWORD_LENGTH, 6 | minMemberNameLength: 1, 7 | maxMemberNameLength: 8, 8 | maxPrice: 10000000, 9 | minBillNameLength: 1, 10 | maxBillNameLength: 30, 11 | minAccountNumberLength: 8, 12 | maxAccountNumberLength: 30, 13 | }; 14 | 15 | export default RULE; 16 | -------------------------------------------------------------------------------- /client/src/hooks/useUserInfoContext.ts: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | 3 | import {UserInfoContext} from '@components/Loader/UserInfo/UserInfoProvider'; 4 | 5 | const useUserInfoContext = () => { 6 | const value = useContext(UserInfoContext); 7 | 8 | if (!value) { 9 | throw new Error('UserInfoProvider와 함께 사용해주세요.'); 10 | } 11 | 12 | return value; 13 | }; 14 | 15 | export default useUserInfoContext; 16 | -------------------------------------------------------------------------------- /server/src/docs/asciidoc/memberBillReport.adoc: -------------------------------------------------------------------------------- 1 | == 정산 2 | 3 | === 참여자별 정산 결과 조회 4 | 5 | operation::getMemberBillReports[snippets="path-parameters,http-request,response-body,response-fields,http-response,http-request"] 6 | 7 | ==== [.red]#Exceptions# 8 | 9 | [source,json,options="nowrap"] 10 | ---- 11 | [ 12 | { 13 | "code":"EVENT_NOT_FOUND", 14 | "message":"존재하지 않는 행사입니다." 15 | } 16 | ] 17 | ---- 18 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconX.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import IconXSvg from '@assets/image/x.svg'; 3 | 4 | import {SvgProps} from '../Icon.type'; 5 | import Svg from '../Svg'; 6 | 7 | export const IconX = ({color = 'gray', ...rest}: Omit) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/common/auth/Login.java: -------------------------------------------------------------------------------- 1 | package haengdong.common.auth; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.PARAMETER) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface Login { 11 | boolean required() default true; 12 | } 13 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/user/application/request/UserJoinAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.user.application.request; 2 | 3 | import haengdong.user.domain.User; 4 | 5 | public record UserJoinAppRequest( 6 | String memberNumber, 7 | String nickname, 8 | String picture 9 | ) { 10 | public User toUser() { 11 | return User.createMember(nickname, memberNumber, picture); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/components/Design/components/DepositCheck/DepositCheck.type.ts: -------------------------------------------------------------------------------- 1 | export interface DepositCheckStyleProps { 2 | isDeposited: boolean; 3 | } 4 | 5 | export interface DepositCheckCustomProps { 6 | isDeposited: boolean; 7 | } 8 | 9 | export type DepositCheckOptionProps = DepositCheckStyleProps & DepositCheckCustomProps; 10 | 11 | export type DepositCheckProps = React.ComponentProps<'div'> & DepositCheckOptionProps; 12 | -------------------------------------------------------------------------------- /client/src/hooks/useEventDataContext.tsx: -------------------------------------------------------------------------------- 1 | import {useContext} from 'react'; 2 | 3 | import {EventDataContext} from '@components/Loader/EventData/EventDataProvider'; 4 | 5 | const useEventDataContext = () => { 6 | const value = useContext(EventDataContext); 7 | 8 | if (!value) { 9 | throw new Error('EventDataProvider와 함께 사용해주세요.'); 10 | } 11 | 12 | return value; 13 | }; 14 | 15 | export default useEventDataContext; 16 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconEdit.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import IconEditSvg from '@assets/image/edit.svg'; 3 | 4 | import {SvgProps} from '../Icon.type'; 5 | import Svg from '../Svg'; 6 | export const IconEdit = ({color = 'gray', ...rest}: Omit) => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/EventAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import haengdong.event.domain.event.Event; 4 | 5 | public record EventAppResponse( 6 | String token, 7 | Long userId 8 | ) { 9 | 10 | public static EventAppResponse of(Event event) { 11 | return new EventAppResponse(event.getToken(), event.getUserId()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Textarea/Textarea.type.ts: -------------------------------------------------------------------------------- 1 | export interface TextareaStyleProps { 2 | height?: string; 3 | } 4 | 5 | export interface TextareaCustomProps { 6 | value: string; 7 | maxLength?: number; 8 | placeholder?: string; 9 | } 10 | 11 | export type TextareaOptionProps = TextareaStyleProps & TextareaCustomProps; 12 | 13 | export type TextareaProps = React.ComponentProps<'textarea'> & TextareaOptionProps; 14 | -------------------------------------------------------------------------------- /client/src/constants/queryKeys.ts: -------------------------------------------------------------------------------- 1 | const QUERY_KEYS = { 2 | steps: 'steps', 3 | event: 'event', 4 | allMembers: 'allMembers', 5 | currentMembers: 'currentMembers', 6 | reports: 'reports', 7 | billDetails: 'billDetails', 8 | images: 'images', 9 | kakaoClientId: 'kakao-client-id', 10 | kakaoLogin: 'kakao-login', 11 | userInfo: 'user-info', 12 | createdEvents: 'createdEvents', 13 | }; 14 | 15 | export default QUERY_KEYS; 16 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Dropdown/useDropdown.ts: -------------------------------------------------------------------------------- 1 | import {useRef, useState} from 'react'; 2 | 3 | const useDropdown = () => { 4 | const [isOpen, setIsOpen] = useState(false); 5 | const baseRef = useRef(null); 6 | const dropdownRef = useRef(null); 7 | 8 | return { 9 | isOpen, 10 | setIsOpen, 11 | baseRef, 12 | dropdownRef, 13 | }; 14 | }; 15 | 16 | export default useDropdown; 17 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconTrash.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import IconTrashSvg from '@assets/image/trash.svg'; 3 | 4 | import {SvgProps} from '../Icon.type'; 5 | import Svg from '../Svg'; 6 | 7 | export const IconTrash = ({color = 'black', ...rest}: Omit) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /.idea/modules/haengdong.main.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconCheck.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import IconCheckSvg from '@assets/image/check.svg'; 3 | 4 | import {SvgProps} from '../Icon.type'; 5 | import Svg from '../Svg'; 6 | 7 | export const IconCheck = ({color = 'primary', ...rest}: Omit) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconKakao.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import IconKakaoSvg from '@assets/image/kakao.svg'; 3 | 4 | import {SvgProps} from '../Icon.type'; 5 | import Svg from '../Svg'; 6 | 7 | export const IconKakao = ({color = 'onKakao', ...rest}: Omit) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /client/src/pages/landing/Section/CreatorSection/Avatar.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const avatarStyle = css({ 4 | display: 'flex', 5 | flexDirection: 'column', 6 | alignItems: 'center', 7 | gap: '0.5rem', 8 | '@media (min-width: 1200px)': { 9 | gap: '1rem', 10 | }, 11 | }); 12 | 13 | export const avatarImageStyle = css({ 14 | width: '100%', 15 | height: '100%', 16 | borderRadius: '25%', 17 | }); 18 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/infrastructure/UUIDProvider.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.infrastructure; 2 | 3 | import java.util.UUID; 4 | import org.springframework.stereotype.Component; 5 | import haengdong.event.domain.RandomValueProvider; 6 | 7 | @Component 8 | public class UUIDProvider implements RandomValueProvider { 9 | 10 | public String createRandomValue() { 11 | return UUID.randomUUID().toString(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/UnPredictableErrorBoundary.tsx: -------------------------------------------------------------------------------- 1 | import {ErrorBoundary} from 'react-error-boundary'; 2 | 3 | import {StrictPropsWithChildren} from '@type/strictPropsWithChildren'; 4 | import ErrorPage from '@pages/fallback/ErrorPage'; 5 | 6 | const UnPredictableErrorBoundary = ({children}: StrictPropsWithChildren) => { 7 | return }>{children}; 8 | }; 9 | 10 | export default UnPredictableErrorBoundary; 11 | -------------------------------------------------------------------------------- /client/src/components/AmplitudeInitializer/AmplitudeInitializer.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react'; 2 | import {init} from '@amplitude/analytics-browser'; 3 | 4 | const AmplitudeInitializer = ({children}: React.PropsWithChildren) => { 5 | useEffect(() => { 6 | init(process.env.AMPLITUDE_KEY, undefined, { 7 | defaultTracking: true, 8 | }); 9 | }, []); 10 | 11 | return children; 12 | }; 13 | 14 | export default AmplitudeInitializer; 15 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconSetting.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import IconSettingSvg from '@assets/image/setting.svg'; 3 | 4 | import {SvgProps} from '../Icon.type'; 5 | import Svg from '../Svg'; 6 | 7 | export const IconSetting = ({color = 'black', ...rest}: Omit) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconXCircle.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import IconXCircleSvg from '@assets/image/x-circle.svg'; 3 | 4 | import {SvgProps} from '../Icon.type'; 5 | import Svg from '../Svg'; 6 | 7 | export const IconXCircle = ({color = 'gray', ...rest}: Omit) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /client/src/components/Design/components/SendButton/SendButton.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | import {Theme} from '@components/Design/theme/theme.type'; 4 | 5 | export const sendButtonStyle = (theme: Theme, disabled: boolean) => 6 | css({ 7 | width: '3.25rem', 8 | height: '1.5rem', 9 | 10 | backgroundColor: disabled ? theme.colors.grayContainer : theme.colors.tertiary, 11 | 12 | borderRadius: '0.5rem', 13 | }); 14 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Container/Container.type.ts: -------------------------------------------------------------------------------- 1 | import {CSSProperties, HTMLAttributes} from 'react'; 2 | 3 | export interface ContainerProps extends HTMLAttributes { 4 | maxW?: CSSProperties['maxWidth']; 5 | p?: CSSProperties['padding']; 6 | m?: CSSProperties['margin']; 7 | br?: CSSProperties['borderRadius']; 8 | b?: CSSProperties['border']; 9 | bg?: CSSProperties['background']; 10 | center?: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /client/src/components/Design/components/DepositToggle/DepositToggle.type.ts: -------------------------------------------------------------------------------- 1 | export interface DepositToggleStyleProps { 2 | isDeposit: boolean; 3 | } 4 | 5 | export interface DepositToggleCustomProps { 6 | isDeposit: boolean; 7 | onToggle: () => void; 8 | } 9 | 10 | export type DepositToggleOptionProps = DepositToggleStyleProps & DepositToggleCustomProps; 11 | 12 | export type DepositToggleProps = React.ComponentProps<'div'> & DepositToggleOptionProps; 13 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icon.type.ts: -------------------------------------------------------------------------------- 1 | import {ColorKeys} from '@token/colors'; 2 | 3 | export type IconColor = ColorKeys; 4 | export interface SvgProps extends React.ComponentProps<'svg'> { 5 | color?: IconColor; 6 | height?: number; 7 | width?: number; 8 | size?: number; 9 | isUsingFill?: boolean; 10 | viewBox?: string; 11 | direction?: Direction; 12 | } 13 | 14 | export type Direction = 'up' | 'right' | 'down' | 'left'; 15 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Select/Select.type.ts: -------------------------------------------------------------------------------- 1 | export type SelectInputProps = Omit, 'onSelect'> & { 2 | labelText?: string; 3 | placeholder?: string; 4 | hasFocus?: boolean; 5 | setHasFocus?: React.Dispatch>; 6 | }; 7 | 8 | export type SelectProps = SelectInputProps & { 9 | defaultValue?: T; 10 | options: T[]; 11 | onSelect: (option: T) => void; 12 | }; 13 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconMeatballs.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import IconMeatballsSvg from '@assets/image/meatballs.svg'; 3 | 4 | import {SvgProps} from '../Icon.type'; 5 | import Svg from '../Svg'; 6 | 7 | export const IconMeatballs = ({color = 'black', ...rest}: Omit) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /client/src/components/Loader/UserInfo/UserInfoProvider.tsx: -------------------------------------------------------------------------------- 1 | import {createContext, PropsWithChildren} from 'react'; 2 | 3 | import {User} from 'types/serviceType'; 4 | 5 | export const UserInfoContext = createContext(null); 6 | 7 | const UserInfoProvider = ({children, ...props}: PropsWithChildren) => { 8 | return {children}; 9 | }; 10 | 11 | export default UserInfoProvider; 12 | -------------------------------------------------------------------------------- /client/src/constants/sessionStorageKeys.ts: -------------------------------------------------------------------------------- 1 | const SESSION_STORAGE_KEYS = { 2 | closeAccountBannerByEventToken: (eventToken: string) => `closeAccountBanner-${eventToken}`, 3 | closeDepositStateBannerByEventToken: (eventToken: string) => `closeDepositStateBanner-${eventToken}`, 4 | createdByGuest: 'createdByGuest', 5 | previousUrlForLogin: 'previousUrlForLogin', 6 | eventHomeTab: 'eventHomeTab', 7 | } as const; 8 | 9 | export default SESSION_STORAGE_KEYS; 10 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/user/config/KakaoProperties.java: -------------------------------------------------------------------------------- 1 | package haengdong.user.config; 2 | 3 | import org.springframework.boot.context.properties.ConfigurationProperties; 4 | 5 | @ConfigurationProperties("kakao") 6 | public record KakaoProperties( 7 | String baseUri, 8 | String clientId, 9 | String tokenRequestUri, 10 | String unlinkRequestUri, 11 | String oauthCodeUri, 12 | String adminKey 13 | ) { 14 | } 15 | -------------------------------------------------------------------------------- /client/src/pages/setting/withdraw/ReasonStep.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const stepButtonBoxStyle = () => 4 | css({ 5 | display: 'flex', 6 | flexDirection: 'row', 7 | justifyContent: 'space-between', 8 | alignItems: 'center', 9 | cursor: 'pointer', 10 | }); 11 | 12 | export const stepButtonGroupStyle = () => 13 | css({ 14 | display: 'flex', 15 | flexDirection: 'column', 16 | gap: '1rem', 17 | }); 18 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Image/Image.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | export type ImageProps = React.ComponentProps<'img'> & { 3 | src: string; 4 | fallbackSrc?: string; 5 | }; 6 | 7 | const Image = ({src, fallbackSrc, ...htmlProps}: ImageProps) => { 8 | return ( 9 | 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default Image; 17 | -------------------------------------------------------------------------------- /client/src/utils/getEventPageUrlByEnvironment.ts: -------------------------------------------------------------------------------- 1 | import {ROUTER_URLS} from '@constants/routerUrls'; 2 | 3 | type EventPageTab = 'home' | 'admin'; 4 | 5 | const getEventPageUrlByEnvironment = (eventId: string, tab: EventPageTab) => { 6 | const isDevelopment = process.env.NODE_ENV === 'development'; 7 | 8 | return `https://${isDevelopment ? 'dev.' : ''}haengdong.pro${ROUTER_URLS.event}/${eventId}/${tab}`; 9 | }; 10 | 11 | export default getEventPageUrlByEnvironment; 12 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconErrorCircle.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import IconErrorCircleSvg from '@assets/image/error-circle.svg'; 3 | 4 | import {SvgProps} from '../Icon.type'; 5 | import Svg from '../Svg'; 6 | 7 | export const IconErrorCircle = ({color = 'error', ...rest}: Omit) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /client/src/pages/event/[eventId]/admin/AuthGate.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react'; 2 | 3 | import useRequestPostAuthentication from '@hooks/queries/auth/useRequestPostAuthentication'; 4 | 5 | const AuthGate = ({children}: React.PropsWithChildren) => { 6 | const {postAuthenticate} = useRequestPostAuthentication(); 7 | 8 | useEffect(() => { 9 | postAuthenticate(); 10 | }, [postAuthenticate]); 11 | 12 | return children; 13 | }; 14 | 15 | export default AuthGate; 16 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/EventImageResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import haengdong.event.application.response.EventImageUrlAppResponse; 4 | 5 | public record EventImageResponse( 6 | Long id, 7 | String url 8 | ) { 9 | 10 | public static EventImageResponse of(EventImageUrlAppResponse response) { 11 | return new EventImageResponse(response.id(), response.url()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconConfirmCircle.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import IconConfirmCircleSvg from '@assets/image/confirm-circle.svg'; 3 | 4 | import {SvgProps} from '../Icon.type'; 5 | import Svg from '../Svg'; 6 | export const IconConfirmCircle = ({color = 'complete', ...rest}: Omit) => { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ListItem/Row.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {useTheme} from '@components/Design/theme/HDesignProvider'; 3 | 4 | import {rowStyle} from './ListItem.style'; 5 | 6 | const Row = ({children, ...rest}: React.HTMLAttributes) => { 7 | const {theme} = useTheme(); 8 | return ( 9 |
    10 | {children} 11 |
    12 | ); 13 | }; 14 | 15 | export default Row; 16 | -------------------------------------------------------------------------------- /client/src/components/Design/layouts/FunnelLayout.tsx: -------------------------------------------------------------------------------- 1 | import {Flex} from '..'; 2 | 3 | const FunnelLayout = ({children}: React.PropsWithChildren) => { 4 | return ( 5 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export default FunnelLayout; 20 | -------------------------------------------------------------------------------- /client/src/global.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg'; 2 | declare module '*.png'; 3 | declare module '*.webp'; 4 | 5 | declare namespace NodeJS { 6 | interface ProcessEnv { 7 | readonly NODE_ENV: 'development' | 'production' | 'test'; 8 | 9 | // env keys 10 | readonly API_BASE_URL: string; 11 | readonly AMPLITUDE_KEY: string; 12 | readonly KAKAO_JAVASCRIPT_KEY: string; 13 | readonly KAKAO_REDIRECT_URI: string; 14 | readonly IMAGE_URL: string; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconPictureSquare.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import IconPictureSquareSvg from '@assets/image/picture-square.svg'; 3 | 4 | import {SvgProps} from '../Icon.type'; 5 | import Svg from '../Svg'; 6 | 7 | export const IconPictureSquare = ({color = 'white', ...rest}: Omit) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/common/properties/CookieProperties.java: -------------------------------------------------------------------------------- 1 | package haengdong.common.properties; 2 | 3 | import java.time.Duration; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | 6 | @ConfigurationProperties("cookie") 7 | public record CookieProperties( 8 | boolean httpOnly, 9 | boolean secure, 10 | String domain, 11 | String path, 12 | String sameSite, 13 | Duration maxAge 14 | ) { 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/EventImageSaveAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import haengdong.event.domain.event.image.EventImage; 4 | 5 | public record EventImageSaveAppResponse( 6 | Long id, 7 | String name 8 | ) { 9 | 10 | public static EventImageSaveAppResponse of(EventImage eventImage) { 11 | return new EventImageSaveAppResponse(eventImage.getId(), eventImage.getName()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/MemberSaveResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import haengdong.event.application.response.MemberSaveAppResponse; 4 | 5 | public record MemberSaveResponse( 6 | Long id, 7 | String name 8 | ) { 9 | 10 | public static MemberSaveResponse of(MemberSaveAppResponse response) { 11 | return new MemberSaveResponse(response.id(), response.name().getValue()); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Tabs/useTabContext.ts: -------------------------------------------------------------------------------- 1 | import {createContext, useContext} from 'react'; 2 | 3 | type TabContextType = { 4 | activeTabIndex: number; 5 | }; 6 | 7 | export const TabContext = createContext(null); 8 | 9 | export const useTabContext = () => { 10 | const context = useContext(TabContext); 11 | 12 | if (!context) { 13 | throw new Error('useTabContext는 TabContext 내부에서 사용되어야 합니다.'); 14 | } 15 | 16 | return context; 17 | }; 18 | -------------------------------------------------------------------------------- /client/src/components/Toast/Toast.type.ts: -------------------------------------------------------------------------------- 1 | import {ToastMessage, ToastOptions} from 'types/toastType'; 2 | 3 | export type ToastType = 'error' | 'confirm' | 'none'; 4 | 5 | export type ToastOptionProps = ToastOptions & { 6 | type?: ToastType; 7 | onClose?: () => void; 8 | onUndo?: () => void; 9 | }; 10 | 11 | export type ToastRequiredProps = { 12 | message: ToastMessage; 13 | }; 14 | 15 | export type ToastProps = React.ComponentProps<'div'> & ToastOptionProps & ToastRequiredProps; 16 | -------------------------------------------------------------------------------- /client/src/utils/isArraysEqual.ts: -------------------------------------------------------------------------------- 1 | const isArraysEqual = (arr1: T[], arr2: T[]) => { 2 | if (arr1.length !== arr2.length) return false; 3 | 4 | // 배열을 정렬한 후 비교 5 | const sortedArr1 = [...arr1].sort(); 6 | const sortedArr2 = [...arr2].sort(); 7 | 8 | // 값은 모두 같으나 순서를 변경했을 시, false를 반환한다. 9 | for (let i = 0; i < sortedArr1.length; i++) { 10 | if (sortedArr1[i] !== sortedArr2[i]) return false; 11 | } 12 | 13 | return true; 14 | }; 15 | 16 | export default isArraysEqual; 17 | -------------------------------------------------------------------------------- /client/src/utils/validate/validateEventName.ts: -------------------------------------------------------------------------------- 1 | import {ERROR_MESSAGE} from '@constants/errorMessage'; 2 | import RULE from '@constants/rule'; 3 | 4 | import {ValidateResult} from './type'; 5 | 6 | const validateEventName = (name: string): ValidateResult => { 7 | if (name.length > RULE.maxEventNameLength) { 8 | return {isValid: false, errorMessage: ERROR_MESSAGE.eventName}; 9 | } 10 | 11 | return {isValid: true, errorMessage: null}; 12 | }; 13 | 14 | export default validateEventName; 15 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/BillAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import haengdong.event.domain.bill.Bill; 4 | 5 | public record BillAppResponse( 6 | Long id, 7 | String title, 8 | Long price, 9 | boolean isFixed 10 | ) { 11 | 12 | public static BillAppResponse of(Bill bill) { 13 | return new BillAppResponse(bill.getId(), bill.getTitle(), bill.getPrice(), bill.isFixed()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/request/EventUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.request; 2 | 3 | import haengdong.event.application.request.EventUpdateAppRequest; 4 | 5 | public record EventUpdateRequest( 6 | String eventName, 7 | String bankName, 8 | String accountNumber 9 | ) { 10 | 11 | public EventUpdateAppRequest toAppRequest() { 12 | return new EventUpdateAppRequest(eventName, bankName, accountNumber); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/src/docs/asciidoc/index.adoc: -------------------------------------------------------------------------------- 1 | ifndef::snippets[] 2 | :snippets: ../../build/generated-snippets 3 | endif::[] 4 | = 행동대장 5 | :source-highlighter: highlightjs :hardbreaks: 6 | :toc: left :doctype: book :icons: font :toc-title: 전체 API 목록 :toclevels: 2 :sectlinks: 7 | :sectnums: 8 | :sectnumlevels: 2 9 | 10 | 11 | include::{docdir}/event.adoc[] 12 | include::{docdir}/memberBillReport.adoc[] 13 | include::{docdir}/member.adoc[] 14 | include::{docdir}/bill.adoc[] 15 | include::{docdir}/billDetail.adoc[] 16 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/LastBillMemberAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import haengdong.event.domain.event.member.EventMember; 4 | import haengdong.user.domain.Nickname; 5 | 6 | public record LastBillMemberAppResponse(Long id, Nickname name) { 7 | 8 | public static LastBillMemberAppResponse of(EventMember eventMember) { 9 | return new LastBillMemberAppResponse(eventMember.getId(), eventMember.getName()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/MemberAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import haengdong.event.domain.event.member.EventMember; 4 | import haengdong.user.domain.Nickname; 5 | 6 | public record MemberAppResponse( 7 | Long id, 8 | Nickname name 9 | ) { 10 | 11 | public static MemberAppResponse of(EventMember eventMember) { 12 | return new MemberAppResponse(eventMember.getId(), eventMember.getName()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/user/presentation/request/UserUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.user.presentation.request; 2 | 3 | import haengdong.user.application.request.UserUpdateAppRequest; 4 | 5 | public record UserUpdateRequest( 6 | String nickname, 7 | String bankName, 8 | String accountNumber 9 | ) { 10 | public UserUpdateAppRequest toAppRequest(Long userId) { 11 | return new UserUpdateAppRequest(userId, nickname, bankName, accountNumber); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconChevron.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import IconChevronSvg from '@assets/image/chevron.svg'; 3 | 4 | import {Direction, SvgProps} from '../Icon.type'; 5 | import Svg from '../Svg'; 6 | 7 | export const IconChevron = ({color = 'tertiary', direction = 'down', ...rest}: Omit) => { 8 | return ( 9 | 10 | 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /client/src/mocks/handlers/reportHandlers.ts: -------------------------------------------------------------------------------- 1 | import {http, HttpResponse} from 'msw'; 2 | 3 | import {MEMBER_API_PREFIX} from '@apis/endpointPrefix'; 4 | 5 | import {MOCK_API_PREFIX} from '@mocks/mockEndpointPrefix'; 6 | import {reportData} from '@mocks/sharedState'; 7 | 8 | export const reportHandlers = [ 9 | // GET /api/eventId/reports (requestGetMemberReport) 10 | http.get(`${MOCK_API_PREFIX}${MEMBER_API_PREFIX}/:eventId/reports`, () => { 11 | return HttpResponse.json(reportData); 12 | }), 13 | ]; 14 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/domain/event/member/EventMemberRepository.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.domain.event.member; 2 | 3 | import java.util.List; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import haengdong.event.domain.event.Event; 6 | 7 | public interface EventMemberRepository extends JpaRepository { 8 | 9 | List findAllByEvent(Event event); 10 | 11 | boolean existsByEventAndIsDeposited(Event event, boolean isDeposited); 12 | } 13 | -------------------------------------------------------------------------------- /client/src/components/Design/components/BankSelect/BankIcon.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {BankIconId} from '@constants/bank'; 3 | 4 | type BankIconProps = { 5 | iconId: BankIconId; 6 | size?: number; 7 | } & React.SVGAttributes; 8 | 9 | export const BankIcon = ({iconId, size = 36, className}: BankIconProps) => { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Box/Box.type.ts: -------------------------------------------------------------------------------- 1 | import {CSSProperties} from 'react'; 2 | 3 | export interface BoxProps extends React.HTMLAttributes { 4 | w?: CSSProperties['width']; 5 | h?: CSSProperties['height']; 6 | z?: CSSProperties['zIndex']; 7 | p?: CSSProperties['padding']; 8 | m?: CSSProperties['margin']; 9 | br?: CSSProperties['borderRadius']; 10 | b?: CSSProperties['border']; 11 | bg?: CSSProperties['background']; 12 | fixed?: boolean; 13 | center?: boolean; 14 | } 15 | -------------------------------------------------------------------------------- /client/src/pages/fallback/BillEmptyFallback.tsx: -------------------------------------------------------------------------------- 1 | import {Flex, Text} from '@components/Design'; 2 | 3 | const BillEmptyFallback = () => { 4 | return ( 5 | 6 | 행사가 시작되지 않았어요 7 | 8 | 주최자가 아직 지출 내역을 등록하지 않았어요 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default BillEmptyFallback; 15 | -------------------------------------------------------------------------------- /client/src/utils/validate/validateEventPassword.ts: -------------------------------------------------------------------------------- 1 | import {ERROR_MESSAGE} from '@constants/errorMessage'; 2 | import REGEXP from '@constants/regExp'; 3 | 4 | import {ValidateResult} from './type'; 5 | 6 | const validateEventPassword = (password: string): ValidateResult => { 7 | if (!REGEXP.eventPassword.test(password)) { 8 | return {isValid: false, errorMessage: ERROR_MESSAGE.eventPasswordType}; 9 | } 10 | return {isValid: true, errorMessage: null}; 11 | }; 12 | 13 | export default validateEventPassword; 14 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Icons/IconSearch.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {useTheme} from '@components/Design/theme/HDesignProvider'; 3 | import IconSearchSvg from '@assets/image/search.svg'; 4 | 5 | import {SvgProps} from '../Icon.type'; 6 | import Svg from '../Svg'; 7 | 8 | export const IconSearch = ({color = 'gray', ...rest}: Omit) => { 9 | return ( 10 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /client/src/types/toastType.ts: -------------------------------------------------------------------------------- 1 | import {Theme} from '@components/Design/theme/theme.type'; 2 | 3 | export type ToastPosition = 'bottom' | 'top'; 4 | 5 | export type ToastOptions = { 6 | showingTime?: number; 7 | isAutoClosed?: boolean; 8 | isCloseOnClick?: boolean; 9 | position?: ToastPosition; 10 | bottom?: string; 11 | top?: string; 12 | theme?: Theme; 13 | }; 14 | 15 | export type ToastMessage = string; 16 | 17 | export type ToastArgs = { 18 | message: ToastMessage; 19 | options: ToastOptions; 20 | }; 21 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/MemberSaveAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import haengdong.event.domain.event.member.EventMember; 4 | import haengdong.user.domain.Nickname; 5 | 6 | public record MemberSaveAppResponse( 7 | Long id, 8 | Nickname name 9 | ) { 10 | 11 | public static MemberSaveAppResponse of(EventMember eventMember) { 12 | return new MemberSaveAppResponse(eventMember.getId(), eventMember.getName()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/StepsResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import java.util.List; 4 | import haengdong.event.application.response.StepAppResponse; 5 | 6 | public record StepsResponse( 7 | List steps 8 | ) { 9 | 10 | public static StepsResponse of(List steps) { 11 | return new StepsResponse(steps.stream() 12 | .map(StepResponse::of) 13 | .toList()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/test/java/haengdong/application/ServiceTestSupport.java: -------------------------------------------------------------------------------- 1 | package haengdong.application; 2 | 3 | import org.junit.jupiter.api.extension.ExtendWith; 4 | import org.springframework.boot.test.context.SpringBootTest; 5 | import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; 6 | import haengdong.support.extension.DatabaseCleanerExtension; 7 | 8 | @ExtendWith(DatabaseCleanerExtension.class) 9 | @SpringBootTest(webEnvironment = WebEnvironment.NONE) 10 | abstract class ServiceTestSupport { 11 | } 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 행동대장들의 정산을 간편하게💰행동대장 2 | 3 | ![service introduce](https://github.com/user-attachments/assets/9e51f7a3-0326-4c06-8b03-65aca574c10c) 4 | 5 | ### 인프라 6 | ![infra](https://github.com/user-attachments/assets/c89bcedf-dee1-4c02-a3df-249e112186f6) 7 | 8 | 9 | ### Backend CI/CD 파이프라인 10 | ![backend drawio](https://github.com/user-attachments/assets/3e0d414e-b5cd-4f13-a334-3a26b5c942aa) 11 | 12 | ### Frontend CI/CD 파이프라인 13 | ![front drawio](https://github.com/user-attachments/assets/fc924c43-ea3a-47e3-b455-310afad1e61e) 14 | -------------------------------------------------------------------------------- /client/src/components/Design/components/TextButton/TextButton.type.ts: -------------------------------------------------------------------------------- 1 | import {TextSize} from '../Text/Text.type'; 2 | 3 | export type TextColor = 'black' | 'gray' | 'onTertiary'; 4 | 5 | export interface TextButtonStyleProps { 6 | textColor: TextColor; 7 | } 8 | 9 | export interface TextButtonCustomProps { 10 | textSize: TextSize; 11 | } 12 | 13 | export type TextButtonOptionProps = TextButtonStyleProps & TextButtonCustomProps; 14 | 15 | export type TextButtonProps = React.ComponentProps<'button'> & TextButtonOptionProps; 16 | -------------------------------------------------------------------------------- /client/src/components/Loader/UserInfo/UserInfoLoader.tsx: -------------------------------------------------------------------------------- 1 | import {Outlet} from 'react-router-dom'; 2 | 3 | import useRequestGetUserInfo from '@hooks/queries/user/useRequestGetUserInfo'; 4 | 5 | import UserInfoProvider from './UserInfoProvider'; 6 | 7 | const UserInfoLoader = () => { 8 | const {userInfo} = useRequestGetUserInfo({enableInitialData: false}); 9 | 10 | return ( 11 | 12 | 13 | 14 | ); 15 | }; 16 | 17 | export default UserInfoLoader; 18 | -------------------------------------------------------------------------------- /client/src/hooks/useSearchReports/useSearchReports.tsx: -------------------------------------------------------------------------------- 1 | import useRequestGetReports from '@hooks/queries/report/useRequestGetReports'; 2 | 3 | type UseSearchReportsParams = { 4 | memberName: string; 5 | }; 6 | 7 | const useSearchReports = ({memberName}: UseSearchReportsParams) => { 8 | const {reports} = useRequestGetReports(); 9 | 10 | return { 11 | matchedReports: reports.filter(memberReport => memberReport.memberName.includes(memberName)), 12 | reports, 13 | }; 14 | }; 15 | 16 | export default useSearchReports; 17 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/BillResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import haengdong.event.application.response.BillAppResponse; 4 | 5 | public record BillResponse( 6 | Long id, 7 | String title, 8 | Long price, 9 | boolean isFixed 10 | ) { 11 | 12 | public static BillResponse of(BillAppResponse response) { 13 | return new BillResponse(response.id(), response.title(), response.price(), response.isFixed()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/request/EventLoginRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import haengdong.event.application.request.EventLoginAppRequest; 5 | 6 | public record EventLoginRequest( 7 | 8 | @NotBlank(message = "비밀번호는 공백일 수 없습니다.") 9 | String password 10 | ) { 11 | 12 | public EventLoginAppRequest toAppRequest(String token) { 13 | return new EventLoginAppRequest(token, password); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Dropdown/Dropdown.type.ts: -------------------------------------------------------------------------------- 1 | export type DropdownBase = 'meatballs' | 'button'; 2 | 3 | export type DropdownButtonProps = React.HTMLAttributes & { 4 | text: string; 5 | setIsOpen?: React.Dispatch>; // 내부에서 사용하기 위한 props 외부에서 넣어주지 말 것 6 | }; 7 | 8 | export type DropdownProps = { 9 | base?: DropdownBase; 10 | baseButtonText?: string; 11 | onBaseButtonClick?: () => void; 12 | children: (React.ReactElement | null)[]; 13 | }; 14 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/common/exception/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.common.exception; 2 | 3 | public record ErrorResponse( 4 | String errorCode, 5 | String message 6 | ) { 7 | 8 | public static ErrorResponse of(HaengdongErrorCode errorCode) { 9 | return new ErrorResponse(errorCode.name(), errorCode.getMessage()); 10 | } 11 | 12 | public static ErrorResponse of(HaengdongErrorCode errorCode, String message) { 13 | return new ErrorResponse(errorCode.name(), message); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Chevron/Chevron.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {IconChevron} from '../Icons/Icons/IconChevron'; 3 | 4 | import {chevronStyle, activeChevronStyle} from './Chevron.style'; 5 | 6 | type ChevronProps = { 7 | isActive: boolean; 8 | }; 9 | 10 | const Chevron = ({isActive}: ChevronProps) => { 11 | return ( 12 |
    13 | 14 |
    15 | ); 16 | }; 17 | 18 | export default Chevron; 19 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ListButton/ListButton.style.ts: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {css} from '@emotion/react'; 3 | 4 | import {Theme} from '@theme/theme.type'; 5 | 6 | export const listButtonStyle = (theme: Theme) => 7 | css({ 8 | display: 'flex', 9 | justifyContent: 'space-between', 10 | alignItems: 'center', 11 | width: '100%', 12 | padding: '0.375rem 1rem', 13 | backgroundColor: theme.colors.white, 14 | 15 | boxShadow: `0 1px 0 0 ${theme.colors.grayContainer} inset `, 16 | }); 17 | -------------------------------------------------------------------------------- /client/src/components/Loader/EventData/EventDataLoader.tsx: -------------------------------------------------------------------------------- 1 | import {Outlet} from 'react-router-dom'; 2 | 3 | import useEventLoader from '@hooks/useEventLoader'; 4 | 5 | import EventDataProvider from './EventDataProvider'; 6 | 7 | const EventDataLoader = () => { 8 | const eventData = useEventLoader(); 9 | 10 | if (eventData.isFetching) { 11 | return null; 12 | } 13 | 14 | return ( 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default EventDataLoader; 22 | -------------------------------------------------------------------------------- /client/src/mocks/handlers.ts: -------------------------------------------------------------------------------- 1 | import {authHandler} from './handlers/authHandlers'; 2 | import {eventHandler} from './handlers/eventHandlers'; 3 | import {reportHandlers} from './handlers/reportHandlers'; 4 | import {testHandler} from './handlers/testHandlers'; 5 | import {billHandler} from './handlers/billHandler'; 6 | import {memberHandler} from './handlers/memberHandler'; 7 | 8 | export const handlers = [ 9 | ...authHandler, 10 | ...eventHandler, 11 | ...billHandler, 12 | ...memberHandler, 13 | ...testHandler, 14 | ...reportHandlers, 15 | ]; 16 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/user/application/request/UserGuestSaveAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.user.application.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import haengdong.user.domain.User; 5 | 6 | public record UserGuestSaveAppRequest( 7 | 8 | @NotBlank(message = "닉네임은 공백일 수 없습니다.") 9 | String nickname, 10 | 11 | @NotBlank(message = "비밀번호는 공백일 수 없습니다.") 12 | String password 13 | ) { 14 | public User toUser() { 15 | return User.createGuest(nickname, password); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Profile/Profile.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {useTheme} from '@components/Design/theme/HDesignProvider'; 3 | 4 | import Image from '../Image/Image'; 5 | 6 | import {profileContainerStyle} from './Profile.style'; 7 | import {ProfileProps} from './Profile.type'; 8 | 9 | export const Profile = ({size = 'medium', ...profileProps}: ProfileProps) => { 10 | const {theme} = useTheme(); 11 | 12 | return ; 13 | }; 14 | -------------------------------------------------------------------------------- /client/src/components/Logo/RunningDogLogo.tsx: -------------------------------------------------------------------------------- 1 | import Image from '@components/Design/components/Image/Image'; 2 | 3 | import getImageUrl from '@utils/getImageUrl'; 4 | 5 | import {logoImageStyle, logoStyle} from './Logo.style'; 6 | 7 | const RunningDogLogo = () => { 8 | return ( 9 |
    10 | 15 |
    16 | ); 17 | }; 18 | 19 | export default RunningDogLogo; 20 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/MemberDepositResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import haengdong.event.application.response.MemberDepositAppResponse; 4 | 5 | public record MemberDepositResponse( 6 | Long id, 7 | String name, 8 | boolean isDeposited 9 | ) { 10 | 11 | public static MemberDepositResponse of(MemberDepositAppResponse response) { 12 | return new MemberDepositResponse(response.id(), response.name().getValue(), response.isDeposited()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ListItem/ListItem.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {useTheme} from '@components/Design/theme/HDesignProvider'; 3 | 4 | import {listItemStyle} from './ListItem.style'; 5 | import Row from './Row'; 6 | 7 | const ListItem = ({children, ...rest}: React.HTMLAttributes) => { 8 | const {theme} = useTheme(); 9 | return ( 10 |
    11 | {children} 12 |
    13 | ); 14 | }; 15 | 16 | ListItem.Row = Row; 17 | 18 | export default ListItem; 19 | -------------------------------------------------------------------------------- /client/src/components/Logo/StandingDogLogo.tsx: -------------------------------------------------------------------------------- 1 | import Image from '@components/Design/components/Image/Image'; 2 | 3 | import getImageUrl from '@utils/getImageUrl'; 4 | 5 | import {logoStyle, logoImageStyle} from './Logo.style'; 6 | 7 | const StandingDogLogo = () => { 8 | return ( 9 |
    10 | 15 |
    16 | ); 17 | }; 18 | 19 | export default StandingDogLogo; 20 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/MemberUpdateAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | 4 | import haengdong.event.domain.event.member.EventMember; 5 | import haengdong.event.domain.event.Event; 6 | import haengdong.user.domain.Nickname; 7 | 8 | public record MemberUpdateAppRequest( 9 | Long id, 10 | Nickname name, 11 | boolean isDeposited 12 | ) { 13 | 14 | public EventMember toMember(Event event) { 15 | return new EventMember(id, event, name, isDeposited); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/components/Design/components/BankSelect/BankSelect.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const bankSelectStyle = css({ 4 | display: 'grid', 5 | gridTemplateColumns: 'repeat(3, 1fr)', 6 | gridRowGap: '1rem', 7 | gridColumnGap: '2rem', 8 | placeItems: 'center', 9 | 10 | height: '100%', 11 | 12 | '@media (min-width: 450px)': { 13 | gridTemplateColumns: 'repeat(4, 1fr)', 14 | }, 15 | 16 | overflowY: 'scroll', 17 | scrollbarWidth: 'none', 18 | 19 | '&::-webkit-scrollbar': { 20 | display: 'none', 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /client/src/components/Design/components/BottomSheet/BottomSheet.type.ts: -------------------------------------------------------------------------------- 1 | import {ComponentPropsWithoutRef} from 'react'; 2 | 3 | import {Theme} from '@theme/theme.type'; 4 | 5 | export interface BottomSheetStyleProps { 6 | theme?: Theme; 7 | } 8 | 9 | export interface BottomSheetCustomProps { 10 | isOpened?: boolean; 11 | onOpen?: () => void; 12 | onClose?: () => void; 13 | } 14 | 15 | export type BottomSheetOptionProps = BottomSheetStyleProps & BottomSheetCustomProps; 16 | 17 | export type BottomSheetProps = ComponentPropsWithoutRef<'div'> & BottomSheetOptionProps; 18 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/MembersUpdateAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | import java.util.List; 4 | import haengdong.event.domain.event.Event; 5 | import haengdong.event.domain.event.member.EventMember; 6 | 7 | public record MembersUpdateAppRequest(List members) { 8 | 9 | public List toMembers(Event event) { 10 | return members.stream() 11 | .map(memberRequest -> memberRequest.toMember(event)) 12 | .toList(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Input/Input.type.ts: -------------------------------------------------------------------------------- 1 | import {Theme} from '@theme/theme.type'; 2 | 3 | export interface InputStyleProps { 4 | theme?: Theme; 5 | isError?: boolean; 6 | } 7 | 8 | export type InputType = 'input' | 'search'; 9 | 10 | export interface InputCustomProps { 11 | inputType?: InputType; 12 | labelText?: string; 13 | errorText?: string | null; 14 | onDelete?: () => void; 15 | } 16 | 17 | export type InputOptionProps = InputStyleProps & InputCustomProps; 18 | 19 | export type InputProps = React.ComponentProps<'input'> & InputOptionProps; 20 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/MembersResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import java.util.List; 4 | import haengdong.event.application.response.MembersDepositAppResponse; 5 | 6 | public record MembersResponse( 7 | List members 8 | ) { 9 | 10 | public static MembersResponse of(MembersDepositAppResponse response) { 11 | return new MembersResponse(response.members().stream() 12 | .map(MemberDepositResponse::of) 13 | .toList()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/EventUpdateAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | public record EventUpdateAppRequest( 4 | String eventName, 5 | String bankName, 6 | String accountNumber 7 | ) { 8 | public boolean isEventNameExist() { 9 | return eventName != null && !eventName.isBlank(); 10 | } 11 | 12 | public boolean isAccountExist() { 13 | return accountNumber != null && !accountNumber.isBlank() 14 | && bankName != null && !bankName.isBlank(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/src/hooks/queries/auth/useRequestGetKakaoClientId.ts: -------------------------------------------------------------------------------- 1 | import {useQuery} from '@tanstack/react-query'; 2 | 3 | import {requestKakaoClientId} from '@apis/request/auth'; 4 | 5 | import QUERY_KEYS from '@constants/queryKeys'; 6 | 7 | const useRequestGetKakaoClientId = () => { 8 | const {refetch, ...rest} = useQuery({ 9 | queryKey: [QUERY_KEYS.kakaoClientId], 10 | queryFn: requestKakaoClientId, 11 | enabled: false, 12 | }); 13 | 14 | return { 15 | requestGetClientId: refetch, 16 | ...rest, 17 | }; 18 | }; 19 | 20 | export default useRequestGetKakaoClientId; 21 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Carousel/CarouselDeleteButton.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {IconX} from '../Icons/Icons/IconX'; 3 | 4 | import {deleteButtonStyle} from './Carousel.style'; 5 | 6 | interface Props { 7 | onClick: () => void; 8 | tabIndex: number; 9 | } 10 | 11 | const CarouselDeleteButton = ({onClick, tabIndex}: Props) => { 12 | return ( 13 | 16 | ); 17 | }; 18 | 19 | export default CarouselDeleteButton; 20 | -------------------------------------------------------------------------------- /client/src/components/Design/components/FixedButton/FixedButton.type.ts: -------------------------------------------------------------------------------- 1 | import {ButtonVariants} from '@HDcomponents/Button/Button.type'; 2 | import {Theme} from '@theme/theme.type'; 3 | 4 | export interface FixedButtonStyleProps { 5 | variants?: ButtonVariants; 6 | theme?: Theme; 7 | } 8 | 9 | export interface ButtonCustomProps { 10 | onDeleteClick?: () => void; 11 | onBackClick?: () => void; 12 | } 13 | 14 | export type FixedButtonOptionProps = FixedButtonStyleProps & ButtonCustomProps; 15 | 16 | export type FixedButtonProps = React.ComponentProps<'button'> & FixedButtonOptionProps; 17 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Icons/Svg.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const svgWrapperStyle = (width?: number, height?: number, size?: number) => { 4 | const w = width ?? size ?? 24; 5 | const h = height ?? size ?? 24; 6 | 7 | return css` 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | width: ${w}px; 12 | height: ${h}px; 13 | `; 14 | }; 15 | 16 | export const svgStyle = css` 17 | width: 100%; 18 | height: 100%; 19 | 20 | svg { 21 | width: 100%; 22 | height: 100%; 23 | } 24 | `; 25 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/MemberDepositAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import haengdong.event.domain.event.member.EventMember; 4 | import haengdong.user.domain.Nickname; 5 | 6 | public record MemberDepositAppResponse( 7 | Long id, 8 | Nickname name, 9 | boolean isDeposited 10 | ) { 11 | 12 | public static MemberDepositAppResponse of(EventMember eventMember) { 13 | return new MemberDepositAppResponse(eventMember.getId(), eventMember.getName(), eventMember.isDeposited()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/store/authStore.ts: -------------------------------------------------------------------------------- 1 | import {create} from 'zustand'; 2 | 3 | type State = { 4 | isAdmin: boolean; 5 | isKakaoUser: boolean; 6 | }; 7 | 8 | type Action = { 9 | updateAuth: (isAdmin: boolean) => void; 10 | updateKakaoAuth: (isKakaoUser: boolean) => void; 11 | }; 12 | 13 | const initialState: State = { 14 | isAdmin: false, 15 | isKakaoUser: false, 16 | }; 17 | 18 | export const useAuthStore = create(set => ({ 19 | ...initialState, 20 | updateAuth: isAdmin => set(() => ({isAdmin})), 21 | updateKakaoAuth: isKakaoUser => set(() => ({isKakaoUser})), 22 | })); 23 | -------------------------------------------------------------------------------- /client/src/store/totalExpenseAmountStore.ts: -------------------------------------------------------------------------------- 1 | import {create} from 'zustand'; 2 | 3 | import {Step as StepType} from 'types/serviceType'; 4 | 5 | import {getTotalExpenseAmount} from '@utils/caculateExpense'; 6 | 7 | type State = { 8 | totalExpenseAmount: number; 9 | }; 10 | 11 | type Action = { 12 | updateTotalExpenseAmount: (steps: StepType[]) => void; 13 | }; 14 | 15 | export const useTotalExpenseAmountStore = create(set => ({ 16 | totalExpenseAmount: 0, 17 | updateTotalExpenseAmount: (steps: StepType[]) => set({totalExpenseAmount: getTotalExpenseAmount(steps)}), 18 | })); 19 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/BillAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | import java.util.List; 4 | import haengdong.event.domain.bill.Bill; 5 | import haengdong.event.domain.event.member.EventMember; 6 | import haengdong.event.domain.event.Event; 7 | 8 | public record BillAppRequest( 9 | String title, 10 | Long price, 11 | List memberIds 12 | ) { 13 | 14 | public Bill toBill(Event event, List eventMembers) { 15 | return Bill.create(event, title, price, eventMembers); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/MembersDepositAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import java.util.List; 4 | import haengdong.event.domain.event.member.EventMember; 5 | 6 | public record MembersDepositAppResponse( 7 | List members 8 | ) { 9 | 10 | public static MembersDepositAppResponse of(List eventMembers) { 11 | return new MembersDepositAppResponse(eventMembers.stream() 12 | .map(MemberDepositAppResponse::of) 13 | .toList()); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/user/application/request/UserUpdateAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.user.application.request; 2 | 3 | public record UserUpdateAppRequest( 4 | Long id, 5 | String nickname, 6 | String bankName, 7 | String accountNumber 8 | ) { 9 | public boolean isNicknameExist() { 10 | return nickname != null && !nickname.isBlank(); 11 | } 12 | 13 | public boolean isAccountExist() { 14 | return accountNumber != null && !accountNumber.isBlank() 15 | && bankName != null && !bankName.isBlank(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Box/Box.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {forwardRef} from 'react'; 3 | 4 | import {BoxProps} from './Box.type'; 5 | import {boxStyle} from './Box.style'; 6 | 7 | export const Box = forwardRef(function Box( 8 | {children, w = 'auto', h = 'auto', z, p, m, br, b, bg, fixed = false, center = false, ...props}, 9 | ref, 10 | ) { 11 | return ( 12 |
    13 | {children} 14 |
    15 | ); 16 | }); 17 | 18 | export default Box; 19 | -------------------------------------------------------------------------------- /client/src/mocks/handlers/testHandlers.ts: -------------------------------------------------------------------------------- 1 | import {HttpResponse, http} from 'msw'; 2 | 3 | export const testHandler = [ 4 | http.post(`/throw-handle-error`, () => { 5 | return HttpResponse.json( 6 | { 7 | errorCode: 'TOKEN_NOT_FOUND', 8 | message: '핸들링되는 테스트 에러입니다.', 9 | }, 10 | {status: 400}, 11 | ); 12 | }), 13 | 14 | http.post(`/throw-unhandle-error`, () => { 15 | return HttpResponse.json( 16 | { 17 | errorCode: 'strange error', 18 | message: '핸들링이 안되는 테스트 에러입니다.', 19 | }, 20 | {status: 500}, 21 | ); 22 | }), 23 | ]; 24 | -------------------------------------------------------------------------------- /client/src/utils/SessionStorage.ts: -------------------------------------------------------------------------------- 1 | const SessionStorage = { 2 | get: (key: string): T | null => { 3 | const savedElement = sessionStorage.getItem(key); 4 | 5 | if (savedElement === null) { 6 | return null; 7 | } 8 | 9 | const element = JSON.parse(savedElement) as T; 10 | return element; 11 | }, 12 | 13 | set: (key: string, data: T) => { 14 | const element = JSON.stringify(data); 15 | sessionStorage.setItem(key, element); 16 | }, 17 | 18 | remove: (key: string) => { 19 | sessionStorage.removeItem(key); 20 | }, 21 | }; 22 | 23 | export default SessionStorage; 24 | -------------------------------------------------------------------------------- /client/src/components/AmountInput/AmountInput.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import type {Meta, StoryObj} from '@storybook/react'; 3 | 4 | import AmountInput from './AmountInput'; 5 | 6 | const meta = { 7 | title: 'Components/AmountInput', 8 | component: AmountInput, 9 | tags: ['autodocs'], 10 | parameters: { 11 | layout: 'centered', 12 | }, 13 | argTypes: {}, 14 | args: { 15 | value: '112,000', 16 | }, 17 | } satisfies Meta; 18 | 19 | export default meta; 20 | 21 | type Story = StoryObj; 22 | 23 | export const Playground: Story = {}; 24 | -------------------------------------------------------------------------------- /client/src/pages/event/[eventId]/admin/AdminPage.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const receiptStyle = () => 4 | css({ 5 | display: 'flex', 6 | flexDirection: 'column', 7 | gap: '0.5rem', 8 | paddingInline: '1rem', 9 | paddingBottom: '2rem', 10 | }); 11 | 12 | export const titleAndListButtonContainerStyle = () => 13 | css({ 14 | display: 'flex', 15 | flexDirection: 'column', 16 | }); 17 | 18 | export const buttonGroupStyle = () => 19 | css({ 20 | display: 'flex', 21 | width: '100%', 22 | padding: '0 0.5rem', 23 | gap: '0.5rem', 24 | }); 25 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/BillDetailsUpdateAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | import haengdong.event.domain.bill.Bill; 4 | import haengdong.event.domain.bill.BillDetail; 5 | import java.util.List; 6 | 7 | public record BillDetailsUpdateAppRequest( 8 | List billDetailUpdateAppRequests 9 | ) { 10 | public List toBillDetails(Bill bill) { 11 | return billDetailUpdateAppRequests.stream() 12 | .map(request -> request.toBillDetail(bill)) 13 | .toList(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/domain/event/EventRepository.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.domain.event; 2 | 3 | import java.util.List; 4 | import java.util.Optional; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.stereotype.Repository; 7 | 8 | @Repository 9 | public interface EventRepository extends JpaRepository { 10 | 11 | Optional findByToken(String token); 12 | 13 | boolean existsByTokenAndUserId(String token, Long userId); 14 | 15 | List findByUserId(Long userId); 16 | 17 | void deleteByUserId(Long hostId); 18 | } 19 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/user/presentation/request/UserGuestSaveRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.user.presentation.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import haengdong.user.application.request.UserGuestSaveAppRequest; 5 | 6 | public record UserGuestSaveRequest( 7 | 8 | @NotBlank(message = "닉네임은 공백일 수 없습니다.") 9 | String nickname, 10 | 11 | @NotBlank(message = "비밀번호는 공백일 수 없습니다.") 12 | String password 13 | ) { 14 | public UserGuestSaveAppRequest toAppRequest() { 15 | return new UserGuestSaveAppRequest(nickname, password); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/EventGuestAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | import haengdong.user.application.request.UserGuestSaveAppRequest; 4 | import haengdong.user.domain.Nickname; 5 | 6 | public record EventGuestAppRequest( 7 | String eventName, 8 | String nickname, 9 | String password 10 | ) { 11 | public UserGuestSaveAppRequest toUserRequest() { 12 | return new UserGuestSaveAppRequest(nickname, password); 13 | } 14 | 15 | public Nickname getNickname() { 16 | return new Nickname(nickname); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/EventImagesResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import haengdong.event.application.response.EventImageUrlAppResponse; 4 | import java.util.List; 5 | 6 | public record EventImagesResponse(List images) { 7 | 8 | public static EventImagesResponse of(List responses) { 9 | List images = responses.stream() 10 | .map(EventImageResponse::of) 11 | .toList(); 12 | 13 | return new EventImagesResponse(images); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Button/Button.type.ts: -------------------------------------------------------------------------------- 1 | import {Theme} from '@theme/theme.type'; 2 | 3 | export type ButtonSize = 'small' | 'medium' | 'semiLarge' | 'large'; 4 | export type ButtonVariants = 'primary' | 'secondary' | 'tertiary' | 'destructive' | 'loading' | 'kakao'; 5 | 6 | export interface ButtonStyleProps { 7 | variants?: ButtonVariants; 8 | size?: ButtonSize; 9 | theme?: Theme; 10 | } 11 | 12 | export interface ButtonCustomProps {} 13 | 14 | export type ButtonOptionProps = ButtonStyleProps & ButtonCustomProps; 15 | 16 | export type ButtonProps = React.ComponentProps<'button'> & ButtonOptionProps; 17 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Container/Container.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {forwardRef} from 'react'; 3 | 4 | import {containerStyle} from './Container.style'; 5 | import {ContainerProps} from './Container.type'; 6 | 7 | export const Container = forwardRef(function Container( 8 | {children, maxW, p, m, br, b, bg, center = false, ...props}, 9 | ref, 10 | ) { 11 | return ( 12 |
    13 | {children} 14 |
    15 | ); 16 | }); 17 | 18 | export default Container; 19 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ListItem/ListItem.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | import {Theme} from '@components/Design/theme/theme.type'; 4 | 5 | export const listItemStyle = (theme: Theme) => 6 | css({ 7 | display: 'flex', 8 | flexDirection: 'column', 9 | width: '100%', 10 | gap: '0.5rem', 11 | backgroundColor: theme.colors.white, 12 | padding: '0.5rem 1rem', 13 | borderRadius: '0.75rem', 14 | }); 15 | 16 | export const rowStyle = (theme: Theme) => 17 | css({ 18 | display: 'flex', 19 | width: '100%', 20 | justifyContent: 'space-between', 21 | }); 22 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/MembersSaveAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import java.util.List; 4 | import haengdong.event.domain.event.member.EventMember; 5 | 6 | public record MembersSaveAppResponse( 7 | List members 8 | ) { 9 | 10 | public static MembersSaveAppResponse of(List eventMembers) { 11 | return new MembersSaveAppResponse( 12 | eventMembers.stream() 13 | .map(MemberSaveAppResponse::of) 14 | .toList() 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/CurrentMembersResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import java.util.List; 4 | import haengdong.event.application.response.MemberAppResponse; 5 | 6 | public record CurrentMembersResponse(List members) { 7 | 8 | public static CurrentMembersResponse of(List currentMembers) { 9 | List responses = currentMembers.stream() 10 | .map(MemberResponse::of) 11 | .toList(); 12 | 13 | return new CurrentMembersResponse(responses); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/EventsMineResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import haengdong.event.application.request.EventMineAppResponse; 4 | import java.util.List; 5 | 6 | public record EventsMineResponse( 7 | List events 8 | ) { 9 | 10 | public static EventsMineResponse of(List responses) { 11 | List events = responses.stream() 12 | .map(EventMineResponse::of) 13 | .toList(); 14 | 15 | return new EventsMineResponse(events); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/jest.polyfills.ts: -------------------------------------------------------------------------------- 1 | import {TextDecoder, TextEncoder} from 'node:util'; 2 | 3 | Object.defineProperties(globalThis, { 4 | TextDecoder: {value: TextDecoder}, 5 | TextEncoder: {value: TextEncoder}, 6 | }); 7 | 8 | import {Blob, File} from 'node:buffer'; 9 | 10 | import {fetch, Headers, FormData, Request, Response} from 'undici'; 11 | 12 | Object.defineProperties(globalThis, { 13 | fetch: {value: fetch, writable: true}, 14 | Blob: {value: Blob}, 15 | File: {value: File}, 16 | Headers: {value: Headers}, 17 | FormData: {value: FormData}, 18 | Request: {value: Request}, 19 | Response: {value: Response}, 20 | }); 21 | -------------------------------------------------------------------------------- /client/src/assets/image/kakao.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/domain/event/image/EventImageRepository.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.domain.event.image; 2 | 3 | import haengdong.event.domain.event.Event; 4 | import java.time.Instant; 5 | import java.util.List; 6 | import org.springframework.data.jpa.repository.JpaRepository; 7 | import org.springframework.stereotype.Repository; 8 | 9 | @Repository 10 | public interface EventImageRepository extends JpaRepository { 11 | 12 | List findAllByEvent(Event event); 13 | 14 | Long countByEvent(Event event); 15 | 16 | List findByCreatedAtAfter(Instant date); 17 | } 18 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/MembersSaveResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import java.util.List; 4 | import haengdong.event.application.response.MembersSaveAppResponse; 5 | 6 | public record MembersSaveResponse( 7 | List members 8 | ) { 9 | 10 | public static MembersSaveResponse of(MembersSaveAppResponse response) { 11 | return new MembersSaveResponse( 12 | response.members().stream() 13 | .map(MemberSaveResponse::of) 14 | .toList() 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/hooks/queries/images/useRequestGetImages.ts: -------------------------------------------------------------------------------- 1 | import {useQuery} from '@tanstack/react-query'; 2 | 3 | import {requestGetImages} from '@apis/request/images'; 4 | 5 | import getEventIdByUrl from '@utils/getEventIdByUrl'; 6 | 7 | import QUERY_KEYS from '@constants/queryKeys'; 8 | 9 | const useRequestGetImages = () => { 10 | const eventId = getEventIdByUrl(); 11 | 12 | const {data, ...rest} = useQuery({ 13 | queryKey: [QUERY_KEYS.images, eventId], 14 | queryFn: () => requestGetImages({eventId}), 15 | }); 16 | 17 | return {images: data?.images ?? [], ...rest}; 18 | }; 19 | 20 | export default useRequestGetImages; 21 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/EventMineResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import haengdong.event.application.request.EventMineAppResponse; 4 | import java.time.LocalDateTime; 5 | 6 | public record EventMineResponse( 7 | String eventId, 8 | String eventName, 9 | boolean isFinished, 10 | LocalDateTime createdAt 11 | ) { 12 | 13 | public static EventMineResponse of(EventMineAppResponse response) { 14 | return new EventMineResponse(response.eventId(), response.eventName(), response.isFinished(), response.createdAt()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/src/apis/request/report.ts: -------------------------------------------------------------------------------- 1 | import type {Reports} from 'types/serviceType'; 2 | 3 | import {WithErrorHandlingStrategy} from '@errors/RequestGetError'; 4 | 5 | import {BASE_URL} from '@apis/baseUrl'; 6 | import {MEMBER_API_PREFIX} from '@apis/endpointPrefix'; 7 | import {requestGet} from '@apis/request'; 8 | import {WithEventId} from '@apis/withId.type'; 9 | 10 | export const requestGetReports = async ({eventId, ...props}: WithEventId) => { 11 | return await requestGet({ 12 | baseUrl: BASE_URL.HD, 13 | endpoint: `${MEMBER_API_PREFIX}/${eventId}/reports`, 14 | ...props, 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Chip/Chip.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | import {ColorKeys} from '@components/Design/token/colors'; 4 | import {Theme} from '@theme/theme.type'; 5 | 6 | interface ChipStyleProps { 7 | theme: Theme; 8 | color: ColorKeys; 9 | } 10 | 11 | export const chipStyle = ({theme, color}: ChipStyleProps) => 12 | css({ 13 | display: 'flex', 14 | padding: '0.125rem 0.5rem ', 15 | borderRadius: '0.5rem', 16 | color: `${theme.colors[color]}`, 17 | boxSizing: 'border-box', 18 | outline: 'none', 19 | boxShadow: `inset 0 0 0 1px ${theme.colors[color]}`, 20 | }); 21 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ExpenseList/ExpenseList.type.ts: -------------------------------------------------------------------------------- 1 | import {Report} from 'types/serviceType'; 2 | 3 | export type ExpenseItemCustomProps = Report & { 4 | onSendButtonClick: (memberId: number, amount: number) => void; 5 | onCopy: (amount: number) => Promise; 6 | canSendBank: boolean; 7 | }; 8 | 9 | export type ExpenseItemProps = Omit, 'onCopy'> & ExpenseItemCustomProps; 10 | 11 | export type ExpenseListProps = { 12 | memberName: string; 13 | onSearch: ({target}: React.ChangeEvent) => void; 14 | placeholder: string; 15 | expenseList: ExpenseItemProps[]; 16 | }; 17 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/BillDetailsAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | import haengdong.event.domain.bill.BillDetail; 6 | 7 | public record BillDetailsAppResponse(List billDetails) { 8 | 9 | public static BillDetailsAppResponse of(List billDetails) { 10 | return billDetails.stream() 11 | .map(BillDetailAppResponse::of) 12 | .collect(Collectors.collectingAndThen(Collectors.toList(), BillDetailsAppResponse::new)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /client/src/pages/landing/Nav/Nav.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const navFixedStyle = css({ 4 | position: 'fixed', 5 | top: 0, 6 | left: 0, 7 | right: 0, 8 | zIndex: 10, 9 | display: 'flex', 10 | flexDirection: 'column', 11 | alignItems: 'center', 12 | padding: '1rem 0', 13 | width: '100%', 14 | backgroundColor: 'white', 15 | }); 16 | 17 | export const navWrapperStyle = css({ 18 | maxWidth: '1200px', 19 | width: '100%', 20 | }); 21 | 22 | export const navStyle = css({ 23 | display: 'flex', 24 | justifyContent: 'space-between', 25 | alignItems: 'center', 26 | height: '2.5rem', 27 | }); 28 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/common/exception/AuthenticationException.java: -------------------------------------------------------------------------------- 1 | package haengdong.common.exception; 2 | 3 | import lombok.Getter; 4 | 5 | @Getter 6 | public class AuthenticationException extends RuntimeException { 7 | 8 | private final HaengdongErrorCode errorCode; 9 | private final String message; 10 | 11 | public AuthenticationException(HaengdongErrorCode errorCode) { 12 | this(errorCode, errorCode.getMessage()); 13 | } 14 | 15 | public AuthenticationException(HaengdongErrorCode errorCode, String message) { 16 | this.errorCode = errorCode; 17 | this.message = message; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/src/components/Design/components/BankSelect/BankSelect.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import type {Meta, StoryObj} from '@storybook/react'; 3 | 4 | import BankSelect from './BankSelect'; 5 | 6 | const meta = { 7 | title: 'Components/BankSelect', 8 | component: BankSelect, 9 | tags: ['autodocs'], 10 | parameters: { 11 | // layout: 'centered', 12 | }, 13 | argTypes: {}, 14 | args: { 15 | onSelect: (name: string) => alert(name), 16 | }, 17 | } satisfies Meta; 18 | 19 | export default meta; 20 | 21 | type Story = StoryObj; 22 | 23 | export const Playground: Story = {}; 24 | -------------------------------------------------------------------------------- /client/src/errors/RequestError.ts: -------------------------------------------------------------------------------- 1 | import {RequestErrorType} from './requestErrorType'; 2 | 3 | class RequestError extends Error { 4 | requestBody; 5 | status; 6 | endpoint; 7 | method; 8 | errorCode; 9 | message; 10 | 11 | constructor({requestBody, status, endpoint, errorCode, method, name, message}: RequestErrorType) { 12 | super(errorCode); 13 | 14 | this.requestBody = requestBody; 15 | this.status = status; 16 | this.endpoint = endpoint; 17 | this.errorCode = errorCode; 18 | this.message = message; 19 | this.method = method; 20 | this.name = name; 21 | } 22 | } 23 | 24 | export default RequestError; 25 | -------------------------------------------------------------------------------- /client/src/apis/request/step.ts: -------------------------------------------------------------------------------- 1 | import {Steps} from 'types/serviceType'; 2 | import {WithErrorHandlingStrategy} from '@errors/RequestGetError'; 3 | 4 | import {BASE_URL} from '@apis/baseUrl'; 5 | import {MEMBER_API_PREFIX} from '@apis/endpointPrefix'; 6 | import {requestGet} from '@apis/request'; 7 | import {WithEventId} from '@apis/withId.type'; 8 | 9 | export const requestGetSteps = async ({eventId, ...props}: WithEventId) => { 10 | const {steps} = await requestGet({ 11 | baseUrl: BASE_URL.HD, 12 | endpoint: `${MEMBER_API_PREFIX}/${eventId}/bills`, 13 | ...props, 14 | }); 15 | 16 | return steps; 17 | }; 18 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/request/BillUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.NotNull; 5 | import haengdong.event.application.request.BillUpdateAppRequest; 6 | 7 | public record BillUpdateRequest( 8 | 9 | @NotBlank(message = "지출 내역 제목은 공백일 수 없습니다.") 10 | String title, 11 | 12 | @NotNull(message = "지출 금액은 공백일 수 없습니다.") 13 | Long price 14 | ) { 15 | 16 | public BillUpdateAppRequest toAppResponse() { 17 | return new BillUpdateAppRequest(title, price); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Amount/Amount.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | 3 | import {css} from '@emotion/react'; 4 | 5 | import Text from '../Text/Text'; 6 | 7 | interface Props { 8 | amount: number; 9 | } 10 | 11 | const Amount = ({amount}: Props) => { 12 | return ( 13 |
    20 | {amount ? amount.toLocaleString('ko-kr') : 0} 21 | 22 | 원 23 | 24 |
    25 | ); 26 | }; 27 | 28 | export default Amount; 29 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ContentLabel/ContentLabel.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | 3 | import TextButton from '../TextButton/TextButton'; 4 | import Text from '../Text/Text'; 5 | 6 | import {ContentLabelProps} from './ContentLabel.type'; 7 | 8 | const ContentLabel = ({children, onClick}: ContentLabelProps) => { 9 | return typeof onClick !== 'undefined' ? ( 10 | 11 | {children} 12 | 13 | ) : ( 14 | 15 | {children} 16 | 17 | ); 18 | }; 19 | 20 | export default ContentLabel; 21 | -------------------------------------------------------------------------------- /client/src/components/Design/components/EditableItem/EditableItem.Input.type.ts: -------------------------------------------------------------------------------- 1 | import {TextSize} from '@HDcomponents/Text/Text.type'; 2 | import {Theme} from '@theme/theme.type'; 3 | 4 | export interface InputStyleProps { 5 | hasError?: boolean; 6 | textSize?: TextSize; 7 | } 8 | 9 | export interface InputCustomProps { 10 | isFixed?: boolean; 11 | value: string | number; 12 | readOnly?: boolean; 13 | } 14 | 15 | export interface InputStylePropsWithTheme extends InputStyleProps { 16 | theme: Theme; 17 | } 18 | 19 | export type InputOptionProps = InputStyleProps & InputCustomProps; 20 | 21 | export type InputProps = React.ComponentProps<'input'> & InputOptionProps; 22 | -------------------------------------------------------------------------------- /client/src/hooks/queries/auth/useRequestGetKakaoLogin.ts: -------------------------------------------------------------------------------- 1 | import {useQuery} from '@tanstack/react-query'; 2 | 3 | import {requestGetKakaoLogin} from '@apis/request/auth'; 4 | 5 | import QUERY_KEYS from '@constants/queryKeys'; 6 | 7 | const useRequestGetKakaoLogin = () => { 8 | const code = new URLSearchParams(location.search).get('code'); 9 | 10 | const {refetch, ...rest} = useQuery({ 11 | queryKey: [QUERY_KEYS.kakaoLogin, code], 12 | queryFn: () => requestGetKakaoLogin(code ?? ''), 13 | enabled: false, 14 | }); 15 | 16 | return { 17 | requestGetKakaoLogin: refetch, 18 | ...rest, 19 | }; 20 | }; 21 | 22 | export default useRequestGetKakaoLogin; 23 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/EventMineAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | import haengdong.event.domain.event.Event; 4 | import java.time.LocalDateTime; 5 | import java.time.ZoneId; 6 | 7 | public record EventMineAppResponse( 8 | String eventId, 9 | String eventName, 10 | boolean isFinished, 11 | LocalDateTime createdAt 12 | ) { 13 | public static EventMineAppResponse of(Event event, boolean isFinished) { 14 | return new EventMineAppResponse(event.getToken(), event.getName(), isFinished, LocalDateTime.ofInstant(event.getCreatedAt(), ZoneId.of("Asia/Seoul")) ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/request/BillDetailUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.request; 2 | 3 | import jakarta.validation.constraints.NotNull; 4 | import haengdong.event.application.request.BillDetailUpdateAppRequest; 5 | 6 | public record BillDetailUpdateRequest( 7 | 8 | @NotNull(message = "지출 상세 ID는 공백일 수 없습니다.") 9 | Long id, 10 | 11 | @NotNull(message = "지출 금액은 공백일 수 없습니다.") 12 | Long price, 13 | 14 | boolean isFixed 15 | ) { 16 | 17 | public BillDetailUpdateAppRequest toAppRequest() { 18 | return new BillDetailUpdateAppRequest(this.id, this.price, this.isFixed); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/assets/image/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /client/src/components/Design/components/TextButton/TextButton.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {forwardRef} from 'react'; 3 | 4 | import Text from '../Text/Text'; 5 | 6 | import {TextButtonProps} from './TextButton.type'; 7 | 8 | export const TextButton: React.FC = forwardRef(function Button( 9 | {textColor, textSize, children, ...htmlProps}: TextButtonProps, 10 | ref, 11 | ) { 12 | return ( 13 | 18 | ); 19 | }); 20 | 21 | export default TextButton; 22 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/EventDetailAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import haengdong.event.domain.event.Event; 4 | import haengdong.user.domain.AccountNumber; 5 | import haengdong.user.domain.Bank; 6 | 7 | public record EventDetailAppResponse( 8 | String eventName, 9 | Bank bankName, 10 | AccountNumber accountNumber, 11 | Boolean createdByGuest 12 | ) { 13 | 14 | public static EventDetailAppResponse of(Event event, UserAppResponse user) { 15 | return new EventDetailAppResponse(event.getName(), event.getBank(), event.getAccountNumber(), user.isGuest()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/MemberBillReportsResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import java.util.List; 4 | import haengdong.event.application.response.MemberBillReportAppResponse; 5 | 6 | public record MemberBillReportsResponse(List reports) { 7 | 8 | public static MemberBillReportsResponse of(List memberBillReports) { 9 | List reports = memberBillReports.stream() 10 | .map(MemberBillReportResponse::of) 11 | .toList(); 12 | 13 | return new MemberBillReportsResponse(reports); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Chip/Chip.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | 3 | import {ColorKeys} from '@components/Design/token/colors'; 4 | import {useTheme} from '@components/Design/theme/HDesignProvider'; 5 | 6 | import Text from '../Text/Text'; 7 | 8 | import {chipStyle} from './Chip.style'; 9 | 10 | interface Props { 11 | color: ColorKeys; 12 | text: string; 13 | } 14 | 15 | const Chip = ({color, text}: Props) => { 16 | const {theme} = useTheme(); 17 | return ( 18 |
    19 | 20 | {text} 21 | 22 |
    23 | ); 24 | }; 25 | 26 | export default Chip; 27 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ChipButton/ChipButton.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | import {ColorKeys} from '@components/Design/token/colors'; 4 | import {Theme} from '@theme/theme.type'; 5 | 6 | interface ChipStyleProps { 7 | theme: Theme; 8 | color: ColorKeys; 9 | } 10 | 11 | export const chipButtonStyle = ({theme, color}: ChipStyleProps) => 12 | css({ 13 | display: 'flex', 14 | padding: '0.25rem 0.375rem 0.25rem 0.75rem', 15 | gap: '0.5rem', 16 | borderRadius: '1rem', 17 | color: `${theme.colors[color]}`, 18 | boxSizing: 'border-box', 19 | outline: 'none', 20 | boxShadow: `inset 0 0 0 1px ${theme.colors[color]}`, 21 | }); 22 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/request/MembersSaveRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.request; 2 | 3 | import haengdong.user.domain.Nickname; 4 | import java.util.List; 5 | import haengdong.event.application.request.MemberSaveAppRequest; 6 | import haengdong.event.application.request.MembersSaveAppRequest; 7 | 8 | public record MembersSaveRequest( 9 | List members 10 | ) { 11 | 12 | public MembersSaveAppRequest toAppRequest() { 13 | return new MembersSaveAppRequest(members.stream() 14 | .map(member -> new MemberSaveAppRequest(new Nickname(member.name()))) 15 | .toList()); 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/request/MembersUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.request; 2 | 3 | import jakarta.validation.Valid; 4 | import jakarta.validation.constraints.NotNull; 5 | import java.util.List; 6 | import haengdong.event.application.request.MembersUpdateAppRequest; 7 | 8 | public record MembersUpdateRequest( 9 | 10 | @Valid 11 | @NotNull 12 | List members 13 | ) { 14 | 15 | public MembersUpdateAppRequest toAppRequest() { 16 | return new MembersUpdateAppRequest(members.stream() 17 | .map(MemberUpdateRequest::toAppRequest) 18 | .toList()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Lottie/Lottie.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | 3 | import {PRIMARY_COLORS} from '@components/Design/token/colors'; 4 | 5 | import {lottieContainerStyle, lottieStyle} from './Lottie.style'; 6 | 7 | const Lottie = () => { 8 | const frameColors = [PRIMARY_COLORS[100], PRIMARY_COLORS[200], PRIMARY_COLORS[300], PRIMARY_COLORS[400]]; 9 | 10 | return ( 11 |
    12 |
    13 |
    14 |
    15 |
    16 |
    17 |
    18 |
    19 |
    20 | ); 21 | }; 22 | 23 | export default Lottie; 24 | -------------------------------------------------------------------------------- /client/src/components/Design/layouts/ContentLayout.tsx: -------------------------------------------------------------------------------- 1 | import {PropsWithChildren} from 'react'; 2 | 3 | import {Flex} from '..'; 4 | 5 | type ContentLayoutBackground = 'white' | 'gray'; 6 | 7 | interface ContentLayoutProps extends PropsWithChildren { 8 | backgroundColor?: ContentLayoutBackground; 9 | } 10 | 11 | export function ContentLayout({backgroundColor, children}: ContentLayoutProps) { 12 | return ( 13 | 22 | {children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /client/src/hooks/useToast/toastEventManager.type.ts: -------------------------------------------------------------------------------- 1 | import {ToastMessage, ToastOptions} from 'types/toastType'; 2 | 3 | const TOAST_SHOW = 'TOAST_SHOW' as const; 4 | const TOAST_CLOSE = 'TOAST_CLOSE' as const; 5 | 6 | export const TOAST_EVENT = { 7 | show: TOAST_SHOW, 8 | close: TOAST_CLOSE, 9 | }; 10 | 11 | export type ToastEventType = typeof TOAST_SHOW | typeof TOAST_CLOSE; 12 | 13 | export type ToastEventCallbackMap = { 14 | [TOAST_SHOW]: (message: ToastMessage, options: ToastOptions) => void; 15 | [TOAST_CLOSE]: () => void; 16 | }; 17 | 18 | export type AddEventHandlerArgs = { 19 | eventType: K; 20 | callback: ToastEventCallbackMap[K]; 21 | }; 22 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/BillDetailsResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import java.util.List; 4 | import java.util.stream.Collectors; 5 | import haengdong.event.application.response.BillDetailsAppResponse; 6 | 7 | public record BillDetailsResponse( 8 | List members 9 | ) { 10 | 11 | public static BillDetailsResponse of(BillDetailsAppResponse billDetailsAppResponse) { 12 | return billDetailsAppResponse.billDetails().stream() 13 | .map(BillDetailResponse::of) 14 | .collect(Collectors.collectingAndThen(Collectors.toList(), BillDetailsResponse::new)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Profile/Profile.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | import {Theme} from '@components/Design/theme/theme.type'; 4 | 5 | import {ProfileSize} from './Profile.type'; 6 | 7 | const SEMANTIC_SIZE: Record = { 8 | small: '1rem', 9 | medium: '1.75rem', 10 | large: '3rem', 11 | }; 12 | 13 | export const profileContainerStyle = (theme: Theme, size: ProfileSize) => { 14 | return css({ 15 | display: 'flex', 16 | 17 | width: SEMANTIC_SIZE[size], 18 | height: SEMANTIC_SIZE[size], 19 | 20 | borderRadius: '50%', 21 | 22 | backgroundColor: theme.colors.white, 23 | 24 | objectFit: 'cover', 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /client/src/pages/fallback/MainPageLoading.tsx: -------------------------------------------------------------------------------- 1 | import Image from '@components/Design/components/Image/Image'; 2 | 3 | import {Flex, Text} from '@components/Design'; 4 | 5 | import getImageUrl from '@utils/getImageUrl'; 6 | 7 | const MainPageLoading = () => { 8 | return ( 9 | 10 | 11 | {`로딩중입니다.\n 잠시만 기다려주세요.`} 15 | 16 | ); 17 | }; 18 | 19 | export default MainPageLoading; 20 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/MemberResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import haengdong.event.application.response.LastBillMemberAppResponse; 4 | import haengdong.event.application.response.MemberAppResponse; 5 | 6 | public record MemberResponse( 7 | Long id, 8 | String name 9 | ) { 10 | 11 | public static MemberResponse of(MemberAppResponse response) { 12 | return new MemberResponse(response.id(), response.name().getValue()); 13 | } 14 | 15 | public static MemberResponse of(LastBillMemberAppResponse response) { 16 | return new MemberResponse(response.id(), response.name().getValue()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Amount/Amount.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import type {Meta, StoryObj} from '@storybook/react'; 3 | 4 | import Amount from '@HDcomponents/Amount/Amount'; 5 | 6 | const meta = { 7 | title: 'Components/Amount', 8 | component: Amount, 9 | tags: ['autodocs'], 10 | parameters: { 11 | layout: 'centered', 12 | }, 13 | argTypes: { 14 | amount: { 15 | description: '', 16 | control: {type: 'number'}, 17 | }, 18 | }, 19 | args: { 20 | amount: 112000, 21 | }, 22 | } satisfies Meta; 23 | 24 | export default meta; 25 | 26 | type Story = StoryObj; 27 | 28 | export const Playground: Story = {}; 29 | -------------------------------------------------------------------------------- /client/src/components/Design/components/EditableItem/useEditableItem.ts: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react'; 2 | 3 | import {useEditableItemContext} from './EditableItem.context'; 4 | 5 | interface UseEditableItemProps { 6 | onInputFocus?: () => void; 7 | onInputBlur?: () => void; 8 | } 9 | 10 | const useEditableItem = ({onInputFocus, onInputBlur}: UseEditableItemProps) => { 11 | const {hasAnyFocus} = useEditableItemContext(); 12 | 13 | useEffect(() => { 14 | if (hasAnyFocus && onInputFocus) { 15 | onInputFocus(); 16 | } 17 | if (!hasAnyFocus && onInputBlur) { 18 | onInputBlur(); 19 | } 20 | }, [hasAnyFocus, onInputFocus, onInputBlur]); 21 | }; 22 | 23 | export default useEditableItem; 24 | -------------------------------------------------------------------------------- /client/src/hooks/usePageBackground.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | 3 | const usePageBackground = () => { 4 | const [isVisible, setIsVisible] = useState(true); 5 | 6 | useEffect(() => { 7 | const handleScroll = () => { 8 | setIsVisible(window.scrollY <= window.innerHeight); 9 | }; 10 | 11 | window.addEventListener('scroll', handleScroll); 12 | window.document.body.style.maxWidth = '100vw'; 13 | return () => { 14 | window.removeEventListener('scroll', handleScroll); 15 | window.document.body.style.maxWidth = '768px'; 16 | }; 17 | }, [window.scrollY, window.innerHeight]); 18 | 19 | return {isVisible}; 20 | }; 21 | 22 | export default usePageBackground; 23 | -------------------------------------------------------------------------------- /client/src/pages/landing/LandingPage.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const landingContainer = css({ 4 | display: 'flex', 5 | flexDirection: 'column', 6 | justifyContent: 'start', 7 | alignItems: 'center', 8 | width: '100vw', 9 | height: '850vh', 10 | overflowX: 'hidden', 11 | }); 12 | 13 | export const backgroundStyle = css({ 14 | position: 'fixed', 15 | height: '100vh', 16 | width: '100vw', 17 | top: 0, 18 | zIndex: -1, 19 | backgroundColor: '#000000', 20 | }); 21 | 22 | export const backgroundImageStyle = (isVisible: boolean) => 23 | css({ 24 | objectFit: 'cover', 25 | height: '100vh', 26 | width: '100vw', 27 | opacity: isVisible ? 1 : 0, 28 | }); 29 | -------------------------------------------------------------------------------- /client/src/components/Modal/BankSelectModal/BankSelectModal.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const bottomSheetStyle = css({ 4 | display: 'flex', 5 | flexDirection: 'column', 6 | gap: '1.5rem', 7 | width: '100%', 8 | height: '100%', 9 | padding: '0 1rem', 10 | }); 11 | 12 | export const bottomSheetHeaderStyle = css({ 13 | display: 'flex', 14 | justifyContent: 'space-between', 15 | alignContent: 'center', 16 | 17 | width: '100%', 18 | padding: '0 0.5rem', 19 | }); 20 | 21 | export const inputContainerStyle = css({ 22 | display: 'flex', 23 | height: '100%', 24 | flexDirection: 'column', 25 | gap: '1.5rem', 26 | overflow: 'auto', 27 | paddingBottom: '4rem', 28 | }); 29 | -------------------------------------------------------------------------------- /client/src/components/StepList/Steps.tsx: -------------------------------------------------------------------------------- 1 | import {Step as StepType} from 'types/serviceType'; 2 | import BillEmptyFallback from '@pages/fallback/BillEmptyFallback'; 3 | 4 | import {Flex} from '@HDesign/index'; 5 | 6 | import Step from './Step'; 7 | 8 | interface Props { 9 | data: StepType[]; 10 | isAdmin: boolean; 11 | } 12 | 13 | const Steps = ({data, isAdmin}: Props) => { 14 | if (data.length <= 0 && !isAdmin) { 15 | return ; 16 | } 17 | 18 | return ( 19 | 20 | {data.map((step, index) => ( 21 | 22 | ))} 23 | 24 | ); 25 | }; 26 | 27 | export default Steps; 28 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/HaengdongApplication.java: -------------------------------------------------------------------------------- 1 | package haengdong; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | import org.springframework.retry.annotation.EnableRetry; 7 | import org.springframework.scheduling.annotation.EnableAsync; 8 | import org.springframework.scheduling.annotation.EnableScheduling; 9 | 10 | @Slf4j 11 | @EnableRetry 12 | @EnableAsync 13 | @EnableScheduling 14 | @SpringBootApplication 15 | public class HaengdongApplication { 16 | 17 | public static void main(String[] args) { 18 | SpringApplication.run(HaengdongApplication.class, args); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/BillDetailAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import haengdong.event.domain.bill.BillDetail; 4 | import haengdong.user.domain.Nickname; 5 | 6 | public record BillDetailAppResponse( 7 | Long id, 8 | Nickname memberName, 9 | Long price, 10 | boolean isFixed 11 | ) { 12 | 13 | public static BillDetailAppResponse of(BillDetail billDetail) { 14 | return new BillDetailAppResponse( 15 | billDetail.getId(), 16 | billDetail.getEventMember().getName(), 17 | billDetail.getPrice(), 18 | billDetail.isFixed() 19 | ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/request/EventGuestSaveRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import haengdong.event.application.request.EventGuestAppRequest; 5 | 6 | public record EventGuestSaveRequest( 7 | 8 | @NotBlank(message = "행사 이름은 공백일 수 없습니다.") 9 | String eventName, 10 | 11 | @NotBlank(message = "행사 이름은 공백일 수 없습니다.") 12 | String nickname, 13 | 14 | @NotBlank(message = "비밀번호는 공백일 수 없습니다.") 15 | String password 16 | ) { 17 | 18 | public EventGuestAppRequest toAppRequest() { 19 | return new EventGuestAppRequest(eventName, nickname, password); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /client/src/pages/event/[eventId]/admin/members/MemberPageType.ts: -------------------------------------------------------------------------------- 1 | import {Report} from 'types/serviceType'; 2 | 3 | interface MemberActions { 4 | changeMemberName: (memberId: number, newName: string) => void; 5 | toggleDepositStatus: (memberId: number) => void; 6 | handleDeleteMember: (memberId: number) => void; 7 | } 8 | 9 | export interface ReturnUseEventMember extends MemberActions { 10 | reports: Report[]; 11 | canSubmit: boolean; 12 | updateMembersOnServer: () => void; 13 | } 14 | 15 | export interface MemberProps extends MemberActions { 16 | member: Report; 17 | } 18 | 19 | export interface MemberNameInputProps { 20 | member: Report; 21 | changeMemberName: (memberId: number, newName: string) => void; 22 | } 23 | -------------------------------------------------------------------------------- /client/src/pages/fallback/SendErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import {useNavigate} from 'react-router-dom'; 2 | 3 | import Top from '@components/Design/components/Top/Top'; 4 | 5 | import {Button, FunnelLayout, MainLayout} from '@HDesign/index'; 6 | 7 | const SendErrorPage = () => { 8 | const navigate = useNavigate(); 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default SendErrorPage; 25 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Banner/Banner.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import type {Meta, StoryObj} from '@storybook/react'; 3 | 4 | import Banner from './Banner'; 5 | 6 | const meta = { 7 | title: 'Components/Banner', 8 | component: Banner, 9 | tags: ['autodocs'], 10 | parameters: { 11 | // layout: 'centered', 12 | }, 13 | argTypes: {}, 14 | args: { 15 | onDelete: () => console.log(''), 16 | title: '계좌번호가 등록되지 않았어요', 17 | description: '계좌번호를 입력해야 참여자가 편하게 송금할 수 있어요', 18 | buttonText: '등록하기', 19 | }, 20 | } satisfies Meta; 21 | 22 | export default meta; 23 | 24 | type Story = StoryObj; 25 | 26 | export const Playground: Story = {}; 27 | -------------------------------------------------------------------------------- /client/src/components/Design/components/DepositCheck/DepositCheck.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from '@storybook/react'; 2 | 3 | import DepositCheck from '@HDcomponents/DepositCheck/DepositCheck'; 4 | 5 | const meta = { 6 | title: 'Components/DepositCheck', 7 | component: DepositCheck, 8 | tags: ['autodocs'], 9 | parameters: { 10 | // layout: 'centered', 11 | }, 12 | argTypes: { 13 | isDeposited: { 14 | description: '', 15 | control: {type: 'boolean'}, 16 | }, 17 | }, 18 | args: { 19 | isDeposited: false, 20 | }, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | 25 | type Story = StoryObj; 26 | 27 | export const Playground: Story = {}; 28 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Text/Text.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import type {TextProps} from '@HDcomponents/Text/Text.type'; 3 | 4 | import React from 'react'; 5 | 6 | import {useTheme} from '@theme/HDesignProvider'; 7 | 8 | import {getSizeStyling} from './Text.style'; 9 | 10 | const Text: React.FC = ({ 11 | size = 'body', 12 | textColor = 'black', 13 | children, 14 | responsive = false, 15 | ...attributes 16 | }: TextProps) => { 17 | const {theme} = useTheme(); 18 | return ( 19 |

    20 | {children === '' ? '\u00A0' : children} 21 |

    22 | ); 23 | }; 24 | 25 | export default Text; 26 | -------------------------------------------------------------------------------- /client/src/components/Design/layouts/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import {PropsWithChildren} from 'react'; 2 | 3 | import {Flex} from '..'; 4 | 5 | type MainLayoutBackground = 'white' | 'gray' | 'lightGray'; 6 | 7 | interface MainLayoutProps extends PropsWithChildren { 8 | backgroundColor?: MainLayoutBackground; 9 | } 10 | 11 | export function MainLayout({backgroundColor, children}: MainLayoutProps) { 12 | return ( 13 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /client/src/components/Reports/Reports.tsx: -------------------------------------------------------------------------------- 1 | import BillEmptyFallback from '@pages/fallback/BillEmptyFallback'; 2 | 3 | import useReportsPage from '@hooks/useReportsPage'; 4 | 5 | import {ExpenseList, Flex} from '@HDesign/index'; 6 | 7 | const Reports = () => { 8 | const {isEmpty, expenseListProp, memberName, changeName} = useReportsPage(); 9 | 10 | if (isEmpty) { 11 | return ; 12 | } 13 | 14 | return ( 15 | 16 | 22 | 23 | ); 24 | }; 25 | 26 | export default Reports; 27 | -------------------------------------------------------------------------------- /client/src/hooks/queries/event/useRequestPostUserEvent.ts: -------------------------------------------------------------------------------- 1 | import {useMutation, useQueryClient} from '@tanstack/react-query'; 2 | 3 | import {requestPostUserEvent} from '@apis/request/event'; 4 | import {EventName} from 'types/serviceType'; 5 | 6 | const useRequestPostUserEvent = () => { 7 | const queryClient = useQueryClient(); 8 | 9 | const {mutateAsync, ...rest} = useMutation({ 10 | mutationFn: (eventName: EventName) => requestPostUserEvent(eventName), 11 | onSuccess: () => { 12 | queryClient.removeQueries(); 13 | }, 14 | }); 15 | 16 | return { 17 | postEvent: mutateAsync, 18 | isPostEventPending: rest.isPending, 19 | ...rest, 20 | }; 21 | }; 22 | 23 | export default useRequestPostUserEvent; 24 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/request/BillDetailsUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.request; 2 | 3 | import jakarta.validation.Valid; 4 | import jakarta.validation.constraints.NotEmpty; 5 | import java.util.List; 6 | import haengdong.event.application.request.BillDetailsUpdateAppRequest; 7 | 8 | public record BillDetailsUpdateRequest( 9 | 10 | @Valid 11 | @NotEmpty 12 | List billDetails 13 | ) { 14 | 15 | public BillDetailsUpdateAppRequest toAppRequest() { 16 | return new BillDetailsUpdateAppRequest(billDetails.stream() 17 | .map(BillDetailUpdateRequest::toAppRequest) 18 | .toList()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Carousel/CarouselIndicator.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {useTheme} from '@components/Design/theme/HDesignProvider'; 3 | 4 | import {indicatorContainerStyle, indicatorStyle} from './Carousel.style'; 5 | 6 | interface Props { 7 | length: number; 8 | currentIndex: number; 9 | } 10 | 11 | const CarouselIndicator = ({length, currentIndex}: Props) => { 12 | const {theme} = useTheme(); 13 | 14 | return ( 15 |
    16 | {Array.from({length}).map((_, index) => ( 17 |
    18 | ))} 19 |
    20 | ); 21 | }; 22 | 23 | export default CarouselIndicator; 24 | -------------------------------------------------------------------------------- /client/src/errors/RequestGetError.ts: -------------------------------------------------------------------------------- 1 | import RequestError from './RequestError'; 2 | import {RequestErrorType} from './requestErrorType'; 3 | 4 | type ErrorHandlingStrategy = 'toast' | 'errorBoundary' | 'ignore'; 5 | 6 | export type WithErrorHandlingStrategy

    = P & { 7 | errorHandlingStrategy?: ErrorHandlingStrategy; 8 | }; 9 | 10 | class RequestGetError extends RequestError { 11 | errorHandlingStrategy: string; 12 | 13 | // errorHandlingType은 기본적으로 제일 많이 사용되는 toast로 했습니다. 14 | constructor({errorHandlingStrategy = 'toast', ...rest}: WithErrorHandlingStrategy) { 15 | super(rest); 16 | 17 | this.errorHandlingStrategy = errorHandlingStrategy; 18 | } 19 | } 20 | 21 | export {RequestGetError}; 22 | -------------------------------------------------------------------------------- /client/src/pages/landing/Section/DescriptionSection/DescriptionSection.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | export const descriptionSectionStyle = css({ 4 | display: 'flex', 5 | flexDirection: 'column', 6 | justifyContent: 'center', 7 | alignItems: 'center', 8 | height: '100vh', 9 | width: '100vw', 10 | gap: '1.5rem', 11 | padding: '3rem 1.5rem', 12 | backgroundColor: 'white', 13 | }); 14 | 15 | export const imgStyle = css({ 16 | width: '10rem', 17 | '@media (min-width: 768px)': { 18 | minWidth: '10rem', 19 | maxWidth: '15rem', 20 | width: '100%', 21 | }, 22 | '@media (min-width: 1600px)': { 23 | minWidth: '15rem', 24 | maxWidth: '20rem', 25 | width: '100%', 26 | }, 27 | }); 28 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/request/MembersSaveAppRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.request; 2 | 3 | import haengdong.event.domain.event.Event; 4 | import haengdong.event.domain.event.member.EventMember; 5 | import haengdong.event.domain.event.member.EventUniqueMembers; 6 | import java.util.List; 7 | 8 | public record MembersSaveAppRequest( 9 | List members 10 | ) { 11 | public EventUniqueMembers toEventMembers(Event event) { 12 | List eventMembers = members.stream() 13 | .map(member -> new EventMember(event, member.name())) 14 | .toList(); 15 | 16 | return new EventUniqueMembers(eventMembers); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/components/Design/components/IconButton/IconButton.type.ts: -------------------------------------------------------------------------------- 1 | import {Theme} from '@theme/theme.type'; 2 | 3 | export type IconButtonSize = 'large' | 'medium' | 'small'; 4 | export type IconButtonVariants = 'none' | 'primary' | 'secondary' | 'tertiary' | 'destructive'; 5 | 6 | export interface IconButtonStyleProps { 7 | size?: IconButtonSize; 8 | variants: IconButtonVariants; 9 | } 10 | 11 | export interface IconButtonStylePropsWithTheme extends IconButtonStyleProps { 12 | theme: Theme; 13 | } 14 | 15 | export interface IconButtonCustomProps {} 16 | 17 | export type IconButtonOptionProps = IconButtonStyleProps & IconButtonCustomProps; 18 | 19 | export type IconButtonProps = React.ComponentProps<'button'> & IconButtonOptionProps; 20 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/UserAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import haengdong.user.domain.AccountNumber; 4 | import haengdong.user.domain.Bank; 5 | import haengdong.user.domain.Nickname; 6 | import haengdong.user.domain.User; 7 | 8 | public record UserAppResponse( 9 | Nickname nickname, 10 | Bank bankName, 11 | AccountNumber accountNumber, 12 | boolean isGuest, 13 | String profileImage 14 | ) { 15 | public static UserAppResponse of(User user) { 16 | return new UserAppResponse(user.getNickname(), user.getBank(), user.getAccountNumber(), user.isGuest(), 17 | user.getPicture() 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/BillDetailResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import haengdong.event.application.response.BillDetailAppResponse; 4 | 5 | public record BillDetailResponse( 6 | Long id, 7 | String memberName, 8 | Long price, 9 | boolean isFixed 10 | ) { 11 | 12 | public static BillDetailResponse of(BillDetailAppResponse billDetailAppResponse) { 13 | return new BillDetailResponse( 14 | billDetailAppResponse.id(), 15 | billDetailAppResponse.memberName().getValue(), 16 | billDetailAppResponse.price(), 17 | billDetailAppResponse.isFixed() 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/components/Design/components/EditableItem/EditableItem.type.ts: -------------------------------------------------------------------------------- 1 | import {Theme} from '@theme/theme.type'; 2 | import {ColorKeys} from '@token/colors'; 3 | 4 | export interface EditableItemStyleProps { 5 | backgroundColor?: ColorKeys; 6 | } 7 | 8 | export interface EditableItemCustomProps { 9 | onInputFocus?: () => void; 10 | onInputBlur?: () => void; 11 | prefixLabelText?: string; 12 | suffixLabelText?: string; 13 | } 14 | 15 | export interface EditableItemStylePropsWithTheme extends EditableItemStyleProps { 16 | theme: Theme; 17 | } 18 | 19 | export type EditableItemOptionProps = EditableItemStyleProps & EditableItemCustomProps; 20 | 21 | export type EditableItemProps = React.ComponentProps<'div'> & EditableItemOptionProps; 22 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Tabs/Tabs.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import type {Meta, StoryObj} from '@storybook/react'; 3 | 4 | import Tabs from '@HDcomponents/Tabs/Tabs'; 5 | 6 | import Tab from './Tab'; 7 | 8 | const meta = { 9 | title: 'Components/Tabs', 10 | component: Tabs, 11 | tags: ['autodocs'], 12 | parameters: { 13 | // layout: 'centered', 14 | }, 15 | args: { 16 | children: [ 17 | 없지롱

    } />, 18 | 있지롱} />, 19 | ], 20 | }, 21 | } satisfies Meta; 22 | 23 | export default meta; 24 | 25 | type Story = StoryObj; 26 | 27 | export const Playground: Story = {}; 28 | -------------------------------------------------------------------------------- /client/src/components/Design/components/EditableItem/EditableItem.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | import {Theme} from '@theme/theme.type'; 4 | import {ColorKeys} from '@token/colors'; 5 | 6 | export const editableItemStyle = (theme: Theme, backgroundColor: ColorKeys) => 7 | css({ 8 | display: 'flex', 9 | justifyContent: 'space-between', 10 | padding: '0.5rem 1rem', 11 | borderRadius: '0.75rem', 12 | backgroundColor: theme.colors[backgroundColor], 13 | }); 14 | 15 | export const labelTextStyle = (theme: Theme, side: 'prefix' | 'suffix') => 16 | css({ 17 | color: theme.colors.gray, 18 | paddingLeft: side === 'prefix' ? '0.375rem' : 0, 19 | paddingRight: side === 'suffix' ? '0.375rem' : 0, 20 | }); 21 | -------------------------------------------------------------------------------- /client/src/components/Footer/Footer.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | import {Theme} from '@components/Design/theme/theme.type'; 4 | import TYPOGRAPHY from '@components/Design/token/typography'; 5 | 6 | export const footerStyle = (theme: Theme) => 7 | css({ 8 | display: 'flex', 9 | flexDirection: 'column', 10 | alignItems: 'center', 11 | gap: '0.625rem', 12 | marginTop: 'auto', 13 | marginBottom: '1.25rem', 14 | color: theme.colors.gray, 15 | 16 | '.footer-link-bundle': { 17 | display: 'flex', 18 | flexDirection: 'row', 19 | gap: '0.625rem', 20 | }, 21 | 22 | a: { 23 | borderBottom: `1px solid ${theme.colors.gray}`, 24 | ...TYPOGRAPHY.tiny, 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/common/infrastructure/DynamicRoutingDataSource.java: -------------------------------------------------------------------------------- 1 | package haengdong.common.infrastructure; 2 | 3 | import static org.springframework.transaction.support.TransactionSynchronizationManager.isCurrentTransactionReadOnly; 4 | 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource; 7 | 8 | @Slf4j 9 | public class DynamicRoutingDataSource extends AbstractRoutingDataSource { 10 | 11 | private static final String PRIMARY = "primary"; 12 | private static final String SECONDARY = "secondary"; 13 | 14 | @Override 15 | protected Object determineCurrentLookupKey() { 16 | return isCurrentTransactionReadOnly() ? SECONDARY : PRIMARY; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/hooks/queries/event/useRequestPatchUser.ts: -------------------------------------------------------------------------------- 1 | import {useMutation, useQueryClient} from '@tanstack/react-query'; 2 | 3 | import {RequestPatchUser, requestPatchUser} from '@apis/request/user'; 4 | 5 | import QUERY_KEYS from '@constants/queryKeys'; 6 | 7 | const useRequestPatchUser = () => { 8 | const queryClient = useQueryClient(); 9 | 10 | const {mutateAsync, ...rest} = useMutation({ 11 | mutationFn: (args: RequestPatchUser) => requestPatchUser({...args}), 12 | 13 | onSuccess: () => { 14 | queryClient.invalidateQueries({ 15 | queryKey: [QUERY_KEYS.userInfo], 16 | }); 17 | }, 18 | }); 19 | 20 | return { 21 | patchUser: mutateAsync, 22 | ...rest, 23 | }; 24 | }; 25 | 26 | export default useRequestPatchUser; 27 | -------------------------------------------------------------------------------- /client/src/utils/detectDevice.ts: -------------------------------------------------------------------------------- 1 | export const isMobileDevice = () => { 2 | const userAgent = window.navigator.userAgent; 3 | const mobileRegex = [/Android/i, /iPhone/i, /iPad/i, /iPod/i, /BlackBerry/i, /Windows Phone/i]; 4 | 5 | return mobileRegex.some(mobile => userAgent.match(mobile)); 6 | }; 7 | 8 | export const isIOS = () => { 9 | const userAgent = window.navigator.userAgent; 10 | const iosRegex = [/iPhone/i, /iPad/i, /iPod/i]; 11 | 12 | return iosRegex.some(device => userAgent.match(device)); 13 | }; 14 | 15 | export const isAndroid = () => { 16 | const userAgent = window.navigator.userAgent; 17 | const androidRegex = [/Android/i, /BlackBerry/i, /Windows Phone/i]; 18 | 19 | return androidRegex.some(device => userAgent.match(device)); 20 | }; 21 | -------------------------------------------------------------------------------- /client/src/hooks/queries/event/useRequestDeleteEvents.ts: -------------------------------------------------------------------------------- 1 | import {useMutation, useQueryClient} from '@tanstack/react-query'; 2 | 3 | import {requestDeleteEvents} from '@apis/request/event'; 4 | import toast from '@hooks/useToast/toast'; 5 | 6 | import QUERY_KEYS from '@constants/queryKeys'; 7 | 8 | const useRequestDeleteEvents = () => { 9 | const queryClient = useQueryClient(); 10 | 11 | const {mutateAsync} = useMutation({ 12 | mutationFn: requestDeleteEvents, 13 | onSuccess: () => { 14 | toast.confirm('행사가 정상적으로 삭제되었습니다'); 15 | queryClient.invalidateQueries({queryKey: [QUERY_KEYS.createdEvents]}); 16 | }, 17 | }); 18 | 19 | return { 20 | deleteEvents: mutateAsync, 21 | }; 22 | }; 23 | 24 | export default useRequestDeleteEvents; 25 | -------------------------------------------------------------------------------- /client/src/pages/main/edit-account/EditUserAccountPage.tsx: -------------------------------------------------------------------------------- 1 | import useRequestPatchUser from '@hooks/queries/event/useRequestPatchUser'; 2 | 3 | import useUserInfoContext from '@hooks/useUserInfoContext'; 4 | 5 | import EditAccountPageView from '@components/EditAccountPageView'; 6 | 7 | import {ROUTER_URLS} from '@constants/routerUrls'; 8 | 9 | const EditUserAccountPage = () => { 10 | const {bankName, accountNumber} = useUserInfoContext(); 11 | const {patchUser} = useRequestPatchUser(); 12 | 13 | return ( 14 | 20 | ); 21 | }; 22 | 23 | export default EditUserAccountPage; 24 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Top/Top.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import type {Meta, StoryObj} from '@storybook/react'; 3 | 4 | import Top from '@HDcomponents/Top/Top'; 5 | 6 | const meta = { 7 | title: 'Components/Top', 8 | component: Top, 9 | tags: ['autodocs'], 10 | parameters: { 11 | layout: 'centered', 12 | }, 13 | argTypes: {}, 14 | args: {}, 15 | } satisfies Meta; 16 | 17 | export default meta; 18 | 19 | type Story = StoryObj; 20 | 21 | export const Playground: Story = { 22 | render: () => { 23 | return ( 24 | 25 | 26 | 27 | 28 | ); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/response/StepResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.response; 2 | 3 | import java.util.List; 4 | import haengdong.event.application.response.StepAppResponse; 5 | 6 | public record StepResponse( 7 | List bills, 8 | List members 9 | ) { 10 | 11 | public static StepResponse of(StepAppResponse response) { 12 | List bills = response.bills().stream() 13 | .map(BillResponse::of) 14 | .toList(); 15 | 16 | List members = response.members().stream() 17 | .map(MemberResponse::of) 18 | .toList(); 19 | return new StepResponse(bills, members); 20 | } 21 | } 22 | 23 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ChipGroup/ChipGroup.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import type {Meta, StoryObj} from '@storybook/react'; 3 | 4 | import ChipGroup from '@HDcomponents/ChipGroup/ChipGroup'; 5 | 6 | const meta = { 7 | title: 'Components/ChipGroup', 8 | component: ChipGroup, 9 | tags: ['autodocs'], 10 | parameters: { 11 | layout: 'centered', 12 | }, 13 | argTypes: { 14 | color: { 15 | description: '', 16 | control: {type: 'select'}, 17 | }, 18 | }, 19 | args: { 20 | color: 'gray', 21 | texts: ['망쵸', '감자', '백호', '이상'], 22 | }, 23 | } satisfies Meta; 24 | 25 | export default meta; 26 | 27 | type Story = StoryObj; 28 | 29 | export const Playground: Story = {}; 30 | -------------------------------------------------------------------------------- /client/src/components/Design/components/IconButton/IconButton.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {forwardRef} from 'react'; 3 | 4 | import {IconButtonProps} from '@HDcomponents/IconButton/IconButton.type'; 5 | import {useTheme} from '@theme/HDesignProvider'; 6 | 7 | import {iconButtonStyle} from './IconButton.style'; 8 | 9 | export const IconButton: React.FC = forwardRef(function Button( 10 | {size, variants, children, ...htmlProps}: IconButtonProps, 11 | ref, 12 | ) { 13 | const {theme} = useTheme(); 14 | 15 | return ( 16 | 19 | ); 20 | }); 21 | 22 | export default IconButton; 23 | -------------------------------------------------------------------------------- /client/src/hooks/createEvent/useCreateGuestEventData.tsx: -------------------------------------------------------------------------------- 1 | import {useState} from 'react'; 2 | 3 | import useMemberName from '@hooks/useMemberName'; 4 | 5 | import useSetEventNameStep from './useSetEventNameStep'; 6 | 7 | // 행사 생성 페이지에서 여러 스텝에 걸쳐 사용되는 상태를 선언해 내려주는 용도의 훅입니다. 8 | const useCreateGuestEventData = () => { 9 | const eventNameProps = useSetEventNameStep(); 10 | const {name: nickname, handleNameChange: handleNicknameChange, ...rest} = useMemberName(); 11 | const [eventToken, setEventToken] = useState(''); 12 | 13 | return { 14 | eventNameProps, 15 | eventToken, 16 | setEventToken, 17 | nicknameProps: { 18 | nickname, 19 | handleNicknameChange, 20 | ...rest, 21 | }, 22 | }; 23 | }; 24 | 25 | export default useCreateGuestEventData; 26 | -------------------------------------------------------------------------------- /client/src/pages/fallback/EventEmptyFallback.tsx: -------------------------------------------------------------------------------- 1 | import Image from '@components/Design/components/Image/Image'; 2 | import VStack from '@components/Design/components/Stack/VStack'; 3 | 4 | import {Text} from '@components/Design'; 5 | 6 | import getImageUrl from '@utils/getImageUrl'; 7 | 8 | const EventEmptyFallback = () => { 9 | return ( 10 | 11 | 12 | {`행사 생성하기 버튼을 눌러\n 행사를 만들어주세요`} 16 | 17 | ); 18 | }; 19 | 20 | export default EventEmptyFallback; 21 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/request/BillSaveRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.request; 2 | 3 | import jakarta.validation.constraints.NotBlank; 4 | import jakarta.validation.constraints.NotEmpty; 5 | import jakarta.validation.constraints.NotNull; 6 | import java.util.List; 7 | import haengdong.event.application.request.BillAppRequest; 8 | 9 | public record BillSaveRequest( 10 | 11 | @NotBlank(message = "지출 내역 제목은 공백일 수 없습니다.") 12 | String title, 13 | 14 | @NotNull(message = "지출 금액은 공백일 수 없습니다.") 15 | Long price, 16 | 17 | @NotEmpty 18 | List memberIds 19 | ) { 20 | 21 | public BillAppRequest toAppRequest() { 22 | return new BillAppRequest(title, price, memberIds); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Carousel/CarouselChangeButton.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import {Direction} from '../Icons/Icon.type'; 3 | import {IconChevron} from '../Icons/Icons/IconChevron'; 4 | 5 | import {changeButtonStyle} from './Carousel.style'; 6 | 7 | interface Props { 8 | direction: Direction; 9 | onClick: () => void; 10 | tabIndex: number; 11 | } 12 | 13 | export const CarouselChangeButton = ({direction, onClick, tabIndex}: Props) => { 14 | return ( 15 | 23 | ); 24 | }; 25 | -------------------------------------------------------------------------------- /client/src/components/Design/components/ChipGroup/ChipGroup.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | 3 | import {ColorKeys} from '@components/Design/token/colors'; 4 | import {useTheme} from '@components/Design/theme/HDesignProvider'; 5 | 6 | import {chipStyle} from '../Chip/Chip.style'; 7 | import Text from '../Text/Text'; 8 | import Chip from '../Chip/Chip'; 9 | 10 | import {chipGroupStyle} from './ChipGroup.style'; 11 | 12 | interface Props { 13 | color: ColorKeys; 14 | texts: string[]; 15 | } 16 | 17 | const ChipGroup = ({color, texts}: Props) => { 18 | return ( 19 |
    20 | {texts.map(text => ( 21 | 22 | ))} 23 |
    24 | ); 25 | }; 26 | 27 | export default ChipGroup; 28 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Title/Title.stories.tsx: -------------------------------------------------------------------------------- 1 | import type {Meta, StoryObj} from '@storybook/react'; 2 | 3 | import Title from '@HDcomponents/Title/Title'; 4 | 5 | const meta = { 6 | title: 'Components/Title', 7 | component: Title, 8 | tags: ['autodocs'], 9 | parameters: { 10 | layout: 'centered', 11 | }, 12 | argTypes: { 13 | title: { 14 | description: '', 15 | control: {type: 'text'}, 16 | }, 17 | amount: { 18 | description: '', 19 | control: {type: 'number'}, 20 | }, 21 | }, 22 | args: { 23 | title: '행동대장 야유회', 24 | amount: 100000, 25 | }, 26 | } satisfies Meta; 27 | 28 | export default meta; 29 | 30 | type Story = StoryObj; 31 | 32 | export const Playground: Story = {}; 33 | -------------------------------------------------------------------------------- /client/src/hooks/queries/member/useRequestGetAllMembers.ts: -------------------------------------------------------------------------------- 1 | import {useQuery} from '@tanstack/react-query'; 2 | 3 | import {requestGetAllMembers} from '@apis/request/member'; 4 | import {WithErrorHandlingStrategy} from '@errors/RequestGetError'; 5 | 6 | import getEventIdByUrl from '@utils/getEventIdByUrl'; 7 | 8 | import QUERY_KEYS from '@constants/queryKeys'; 9 | 10 | const useRequestGetAllMembers = ({...props}: WithErrorHandlingStrategy | null = {}) => { 11 | const eventId = getEventIdByUrl(); 12 | 13 | const {data, ...rest} = useQuery({ 14 | queryKey: [QUERY_KEYS.allMembers, eventId], 15 | queryFn: () => requestGetAllMembers({eventId, ...props}), 16 | }); 17 | 18 | return {members: data?.members ?? [], ...rest}; 19 | }; 20 | 21 | export default useRequestGetAllMembers; 22 | -------------------------------------------------------------------------------- /client/src/hooks/queries/step/useRequestGetSteps.ts: -------------------------------------------------------------------------------- 1 | import {useQuery} from '@tanstack/react-query'; 2 | 3 | import {requestGetSteps} from '@apis/request/step'; 4 | import {WithErrorHandlingStrategy} from '@errors/RequestGetError'; 5 | 6 | import getEventIdByUrl from '@utils/getEventIdByUrl'; 7 | 8 | import QUERY_KEYS from '@constants/queryKeys'; 9 | 10 | const useRequestGetSteps = ({...props}: WithErrorHandlingStrategy | null = {}) => { 11 | const eventId = getEventIdByUrl(); 12 | 13 | const queryResult = useQuery({ 14 | queryKey: [QUERY_KEYS.steps, eventId], 15 | queryFn: () => requestGetSteps({eventId, ...props}), 16 | }); 17 | 18 | return { 19 | steps: queryResult.data ?? [], 20 | ...queryResult, 21 | }; 22 | }; 23 | 24 | export default useRequestGetSteps; 25 | -------------------------------------------------------------------------------- /client/src/hooks/useWithdrawFunnel.ts: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react'; 2 | 3 | export type WithdrawStep = 4 | | 'withdrawReason' 5 | | 'notUseService' 6 | | 'unableToUseDueToError' 7 | | 'cantFigureOutHowToUseIt' 8 | | 'etc' 9 | | 'checkBeforeWithdrawing' 10 | | 'withdrawalCompleted'; 11 | 12 | const useWithdrawFunnel = () => { 13 | const [step, setStep] = useState('checkBeforeWithdrawing'); 14 | 15 | useEffect(() => { 16 | document.body.style.overflow = 'hidden'; 17 | 18 | return () => { 19 | document.body.style.overflow = 'auto'; 20 | }; 21 | }, []); 22 | 23 | const handleMoveStep = (nextStep: WithdrawStep) => setStep(nextStep); 24 | 25 | return {step, handleMoveStep}; 26 | }; 27 | 28 | export default useWithdrawFunnel; 29 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Text/Text.type.ts: -------------------------------------------------------------------------------- 1 | import {Theme} from '@theme/theme.type'; 2 | import {ColorKeys} from '@token/colors'; 3 | 4 | export type TextSize = 5 | | 'head' 6 | | 'title' 7 | | 'subTitle' 8 | | 'body' 9 | | 'smallBody' 10 | | 'caption' 11 | | 'tiny' 12 | | 'bodyBold' 13 | | 'smallBodyBold' 14 | | 'captionBold'; 15 | 16 | export interface TextStyleProps { 17 | size?: TextSize; 18 | textColor?: ColorKeys; 19 | responsive?: boolean; 20 | } 21 | 22 | export interface TextStylePropsWithTheme extends TextStyleProps { 23 | theme: Theme; 24 | } 25 | 26 | export interface TextCustomProps {} 27 | 28 | export type TextOptionProps = TextStyleProps & TextCustomProps; 29 | 30 | export type TextProps = React.ComponentProps<'p'> & TextOptionProps; 31 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/common/domain/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package haengdong.common.domain; 2 | 3 | import jakarta.persistence.Column; 4 | import jakarta.persistence.EntityListeners; 5 | import jakarta.persistence.MappedSuperclass; 6 | import java.time.Instant; 7 | import lombok.Getter; 8 | import org.springframework.data.annotation.CreatedDate; 9 | import org.springframework.data.annotation.LastModifiedDate; 10 | import org.springframework.data.jpa.domain.support.AuditingEntityListener; 11 | 12 | @Getter 13 | @EntityListeners(AuditingEntityListener.class) 14 | @MappedSuperclass 15 | public abstract class BaseEntity { 16 | 17 | @Column(updatable = false) 18 | @CreatedDate 19 | private Instant createdAt; 20 | 21 | @LastModifiedDate 22 | private Instant updatedAt; 23 | } 24 | -------------------------------------------------------------------------------- /server/src/test/java/haengdong/domain/event/PasswordTest.java: -------------------------------------------------------------------------------- 1 | package haengdong.domain.event; 2 | 3 | import static org.assertj.core.api.Assertions.assertThatCode; 4 | 5 | import org.junit.jupiter.api.DisplayName; 6 | import org.junit.jupiter.params.ParameterizedTest; 7 | import org.junit.jupiter.params.provider.ValueSource; 8 | import haengdong.event.domain.event.Password; 9 | import haengdong.common.exception.HaengdongException; 10 | 11 | class PasswordTest { 12 | 13 | @DisplayName("비밀번호는 4자리 숫자 입니다.") 14 | @ParameterizedTest 15 | @ValueSource(strings = {"1", "12", "123", "12345", "adgd"}) 16 | void validatePassword(String rawPassword) { 17 | assertThatCode(() -> new Password(rawPassword)) 18 | .isInstanceOf(HaengdongException.class); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /client/src/utils/detectBrowser.ts: -------------------------------------------------------------------------------- 1 | // https://gurtn.tistory.com/214 2 | const detectBrowser = () => { 3 | const browsers = [ 4 | 'Chrome', 5 | 'Opera', 6 | 'WebTV', 7 | 'Whale', 8 | 'Beonex', 9 | 'Chimera', 10 | 'NetPositive', 11 | 'Phoenix', 12 | 'Firefox', 13 | 'Safari', 14 | 'SkipStone', 15 | 'Netscape', 16 | 'Mozilla', 17 | ]; 18 | 19 | const userAgent = window.navigator.userAgent.toLowerCase(); 20 | 21 | if (userAgent.includes('edg')) { 22 | return 'Edge'; 23 | } 24 | 25 | if (userAgent.includes('trident') || userAgent.includes('msie')) { 26 | return 'Internet Explorer'; 27 | } 28 | 29 | return browsers.find(browser => userAgent.includes(browser.toLowerCase())) || 'Other'; 30 | }; 31 | 32 | export default detectBrowser; 33 | -------------------------------------------------------------------------------- /client/src/components/Design/components/Select/useSelect.ts: -------------------------------------------------------------------------------- 1 | import {useRef, useState} from 'react'; 2 | 3 | type UseSelectProps = { 4 | defaultValue?: T; 5 | onSelect: (option: T) => void; 6 | }; 7 | 8 | const useSelect = ({defaultValue, onSelect}: UseSelectProps) => { 9 | const [isOpen, setIsOpen] = useState(false); 10 | const [value, setValue] = useState(defaultValue); 11 | 12 | const selectRef = useRef(null); 13 | 14 | const handleSelect = (option: T) => { 15 | setValue(option); 16 | onSelect(option); 17 | setIsOpen(false); 18 | }; 19 | 20 | return { 21 | selectRef, 22 | isOpen, 23 | value, 24 | handleSelect, 25 | setIsOpen, 26 | }; 27 | }; 28 | 29 | export default useSelect; 30 | -------------------------------------------------------------------------------- /client/src/components/Loader/EventDataProvider.tsx: -------------------------------------------------------------------------------- 1 | import {createContext, PropsWithChildren} from 'react'; 2 | 3 | import {Event, EventId} from 'types/serviceType'; 4 | 5 | import {useAuthStore} from '@store/authStore'; 6 | 7 | type EventDataContextType = Event & { 8 | eventToken: EventId; 9 | isAdmin: boolean; 10 | }; 11 | 12 | type EventDataProviderProps = PropsWithChildren>; 13 | 14 | export const EventDataContext = createContext(null); 15 | 16 | const EventDataProvider = ({children, ...props}: EventDataProviderProps) => { 17 | const {isAdmin} = useAuthStore(); 18 | 19 | return {children}; 20 | }; 21 | 22 | export default EventDataProvider; 23 | -------------------------------------------------------------------------------- /client/src/components/Loader/EventData/EventDataProvider.tsx: -------------------------------------------------------------------------------- 1 | import {createContext, PropsWithChildren} from 'react'; 2 | 3 | import useEventLoader from '@hooks/useEventLoader'; 4 | 5 | import {useAuthStore} from '@store/authStore'; 6 | 7 | type EventDataContextType = ReturnType & { 8 | isAdmin: boolean; 9 | }; 10 | 11 | type EventDataProviderProps = Omit, 'isAdmin'>; 12 | 13 | export const EventDataContext = createContext(null); 14 | 15 | const EventDataProvider = ({children, ...props}: EventDataProviderProps) => { 16 | const {isAdmin} = useAuthStore(); 17 | 18 | return {children}; 19 | }; 20 | 21 | export default EventDataProvider; 22 | -------------------------------------------------------------------------------- /client/src/hooks/usePriceStep.ts: -------------------------------------------------------------------------------- 1 | import {useCallback} from 'react'; 2 | 3 | import {BillInfo} from '@pages/event/[eventId]/admin/add-bill/AddBillFunnel'; 4 | 5 | import {BillStep} from './useAddBillFunnel'; 6 | 7 | interface Props { 8 | setStep: React.Dispatch>; 9 | setBillInfo: React.Dispatch>; 10 | } 11 | 12 | const usePriceStep = ({setStep, setBillInfo}: Props) => { 13 | const handleNumberKeyboardChange = useCallback( 14 | (value: string) => { 15 | setBillInfo(prev => ({...prev, price: value})); 16 | }, 17 | [setBillInfo], 18 | ); 19 | 20 | const handleNextStep = () => { 21 | setStep('title'); 22 | }; 23 | 24 | return {handleNumberKeyboardChange, handleNextStep}; 25 | }; 26 | 27 | export default usePriceStep; 28 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/application/response/StepAppResponse.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.application.response; 2 | 3 | import java.util.List; 4 | import haengdong.event.domain.step.Step; 5 | 6 | public record StepAppResponse( 7 | List bills, 8 | List members 9 | ) { 10 | 11 | public static StepAppResponse of(Step step) { 12 | List billAppResponses = step.getBills().stream() 13 | .map(BillAppResponse::of) 14 | .toList(); 15 | 16 | List memberAppResponses = step.getMembers().stream() 17 | .map(MemberAppResponse::of) 18 | .toList(); 19 | 20 | return new StepAppResponse(billAppResponses, memberAppResponses); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /server/src/main/java/haengdong/event/presentation/request/MemberUpdateRequest.java: -------------------------------------------------------------------------------- 1 | package haengdong.event.presentation.request; 2 | 3 | import haengdong.user.domain.Nickname; 4 | import jakarta.validation.constraints.NotBlank; 5 | import jakarta.validation.constraints.NotNull; 6 | import haengdong.event.application.request.MemberUpdateAppRequest; 7 | 8 | public record MemberUpdateRequest( 9 | 10 | @NotNull(message = "멤버 ID는 공백일 수 없습니다.") 11 | Long id, 12 | 13 | @NotBlank(message = "멤버 이름은 공백일 수 없습니다.") 14 | String name, 15 | 16 | @NotNull(message = "입금 여부는 공백일 수 없습니다.") 17 | boolean isDeposited 18 | ) { 19 | 20 | public MemberUpdateAppRequest toAppRequest() { 21 | return new MemberUpdateAppRequest(id, new Nickname(name), isDeposited); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /client/src/components/Design/components/DepositCheck/DepositCheck.style.ts: -------------------------------------------------------------------------------- 1 | import {css} from '@emotion/react'; 2 | 3 | import {WithTheme} from '@components/Design/type/withTheme'; 4 | 5 | import {DepositCheckStyleProps} from './DepositCheck.type'; 6 | 7 | export const depositCheckStyle = ({theme, isDeposited}: WithTheme) => 8 | css({ 9 | display: 'flex', 10 | alignItems: 'center', 11 | gap: '0.125rem', 12 | border: `1px solid ${isDeposited ? theme.colors.primary : theme.colors.gray}`, 13 | borderRadius: '0.5rem', 14 | padding: '0.25rem 0.375rem', 15 | height: '1.25rem', 16 | width: 'fit-content', 17 | 18 | '.deposit-check-text': { 19 | color: isDeposited ? theme.colors.primary : theme.colors.gray, 20 | paddingTop: '0.0625rem', 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /client/src/components/Design/components/SendButton/SendButton.stories.tsx: -------------------------------------------------------------------------------- 1 | /** @jsxImportSource @emotion/react */ 2 | import type {Meta, StoryObj} from '@storybook/react'; 3 | 4 | import BankSendButton from './SendButton'; 5 | 6 | const meta = { 7 | title: 'Components/BankSendButton', 8 | component: BankSendButton, 9 | tags: ['autodocs'], 10 | parameters: { 11 | // layout: 'centered', 12 | }, 13 | argTypes: { 14 | isDeposited: { 15 | description: '', 16 | control: {type: 'boolean'}, 17 | }, 18 | }, 19 | args: { 20 | isDeposited: false, 21 | canSend: true, 22 | onClick: () => console.log('안녕'), 23 | }, 24 | } satisfies Meta; 25 | 26 | export default meta; 27 | 28 | type Story = StoryObj; 29 | 30 | export const Playground: Story = {}; 31 | -------------------------------------------------------------------------------- /client/src/hooks/queries/member/useRequestGetCurrentMembers.ts: -------------------------------------------------------------------------------- 1 | import {useQuery} from '@tanstack/react-query'; 2 | 3 | import {requestGetCurrentMembers} from '@apis/request/member'; 4 | import {WithErrorHandlingStrategy} from '@errors/RequestGetError'; 5 | 6 | import getEventIdByUrl from '@utils/getEventIdByUrl'; 7 | 8 | import QUERY_KEYS from '@constants/queryKeys'; 9 | 10 | const useRequestGetCurrentMembers = ({...props}: WithErrorHandlingStrategy | null = {}) => { 11 | const eventId = getEventIdByUrl(); 12 | 13 | const {data, ...rest} = useQuery({ 14 | queryKey: [QUERY_KEYS.currentMembers, eventId], 15 | queryFn: () => requestGetCurrentMembers({eventId, ...props}), 16 | }); 17 | 18 | return {currentMembers: data?.members ?? [], ...rest}; 19 | }; 20 | 21 | export default useRequestGetCurrentMembers; 22 | -------------------------------------------------------------------------------- /client/src/pages/fallback/EventPageLoading.tsx: -------------------------------------------------------------------------------- 1 | import {IconHeundeut} from '@components/Design/components/Icons/Icons/IconHeundeut'; 2 | 3 | import {Flex, IconButton, MainLayout, TopNav} from '@components/Design'; 4 | import {Footer} from '@components/Footer'; 5 | 6 | import {PATHS} from '@constants/routerUrls'; 7 | 8 | const EventPageLoading = () => { 9 | return ( 10 | 11 | 14 | } /> 15 | 16 | 관리 17 | 18 | } 19 | /> 20 |