├── 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 | 
4 |
5 | ### 인프라
6 | 
7 |
8 |
9 | ### Backend CI/CD 파이프라인
10 | 
11 |
12 | ### Frontend CI/CD 파이프라인
13 | 
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 |
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 |
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 |
21 |
22 | );
23 | };
24 |
25 | export default EventPageLoading;
26 |
--------------------------------------------------------------------------------
/server/src/main/java/haengdong/event/presentation/response/MemberBillReportResponse.java:
--------------------------------------------------------------------------------
1 | package haengdong.event.presentation.response;
2 |
3 | import haengdong.event.application.response.MemberBillReportAppResponse;
4 |
5 | public record MemberBillReportResponse(
6 | Long memberId,
7 | String memberName,
8 | boolean isDeposited,
9 | Long price
10 | ) {
11 |
12 | public static MemberBillReportResponse of(MemberBillReportAppResponse memberBillReportAppResponse) {
13 | return new MemberBillReportResponse(
14 | memberBillReportAppResponse.memberId(),
15 | memberBillReportAppResponse.name().getValue(),
16 | memberBillReportAppResponse.isDeposited(),
17 | memberBillReportAppResponse.price()
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/server/src/test/java/haengdong/support/extension/DatabaseCleanerExtension.java:
--------------------------------------------------------------------------------
1 | package haengdong.support.extension;
2 |
3 | import org.junit.jupiter.api.extension.AfterEachCallback;
4 | import org.junit.jupiter.api.extension.ExtensionContext;
5 | import org.springframework.test.context.junit.jupiter.SpringExtension;
6 |
7 | public class DatabaseCleanerExtension implements AfterEachCallback {
8 |
9 | @Override
10 | public void afterEach(ExtensionContext context) {
11 | DatabaseCleaner databaseCleaner = getDataCleaner(context);
12 | databaseCleaner.clear();
13 | }
14 |
15 | private DatabaseCleaner getDataCleaner(ExtensionContext extensionContext) {
16 | return SpringExtension.getApplicationContext(extensionContext)
17 | .getBean(DatabaseCleaner.class);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/components/Design/components/ListButton/ListButton.stories.tsx:
--------------------------------------------------------------------------------
1 | import type {Meta, StoryObj} from '@storybook/react';
2 |
3 | import ListButton from '@HDcomponents/ListButton/ListButton';
4 |
5 | const meta = {
6 | title: 'Components/ListButton',
7 | component: ListButton,
8 | tags: ['autodocs'],
9 | parameters: {
10 | // layout: 'centered',
11 | },
12 | argTypes: {
13 | prefix: {
14 | description: '',
15 | control: {type: 'text'},
16 | },
17 | suffix: {
18 | description: '',
19 | control: {type: 'text'},
20 | },
21 | },
22 | args: {
23 | prefix: '전체 참여자',
24 | suffix: '7명',
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/user/useRequestDeleteUser.ts:
--------------------------------------------------------------------------------
1 | import {useMutation, useQueryClient} from '@tanstack/react-query';
2 |
3 | import {requestDeleteUser} from '@apis/request/user';
4 |
5 | import QUERY_KEYS from '@constants/queryKeys';
6 |
7 | const useRequestDeleteUser = () => {
8 | const queryClient = useQueryClient();
9 |
10 | const {mutateAsync, ...rest} = useMutation({
11 | mutationFn: () => requestDeleteUser(),
12 | onSuccess: () => {
13 | queryClient.invalidateQueries({queryKey: [QUERY_KEYS.kakaoLogin]});
14 | queryClient.invalidateQueries({queryKey: [QUERY_KEYS.kakaoClientId]});
15 | queryClient.invalidateQueries({queryKey: [QUERY_KEYS.userInfo]});
16 | },
17 | });
18 |
19 | return {deleteAsyncUser: mutateAsync, ...rest};
20 | };
21 |
22 | export default useRequestDeleteUser;
23 |
--------------------------------------------------------------------------------
/client/src/components/Design/components/Stack/Stack.type.ts:
--------------------------------------------------------------------------------
1 | import {CSSProperties, HTMLAttributes, ReactNode} from 'react';
2 |
3 | export interface StackProps extends HTMLAttributes {
4 | w?: CSSProperties['width'];
5 | h?: CSSProperties['height'];
6 | p?: CSSProperties['padding'];
7 | m?: CSSProperties['margin'];
8 | br?: CSSProperties['borderRadius'];
9 | b?: CSSProperties['border'];
10 | bg?: CSSProperties['background'];
11 | gap?: string | number;
12 | direction?: CSSProperties['flexDirection'];
13 | justify?: CSSProperties['justifyContent'];
14 | align?: CSSProperties['alignItems'];
15 | divider?: ReactNode;
16 | }
17 |
18 | export type HStackProps = Omit;
19 | export type VStackProps = Omit;
20 |
--------------------------------------------------------------------------------
/server/src/main/java/haengdong/common/exception/HaengdongException.java:
--------------------------------------------------------------------------------
1 | package haengdong.common.exception;
2 |
3 | import lombok.Getter;
4 |
5 | @Getter
6 | public class HaengdongException extends RuntimeException {
7 |
8 | private final HaengdongErrorCode errorCode;
9 |
10 | public HaengdongException(HaengdongErrorCode errorCode) {
11 | super(errorCode.getMessage());
12 | this.errorCode = errorCode;
13 | }
14 |
15 | public HaengdongException(HaengdongErrorCode errorCode, Object... args) {
16 | super(String.format(errorCode.getMessage(), args));
17 | this.errorCode = errorCode;
18 | }
19 |
20 | public HaengdongException(HaengdongErrorCode errorCode, Throwable cause) {
21 | super(errorCode.getMessage(), cause);
22 | this.errorCode = errorCode;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/server/src/main/java/haengdong/event/domain/bill/BillRepository.java:
--------------------------------------------------------------------------------
1 | package haengdong.event.domain.bill;
2 |
3 | import java.util.List;
4 | import java.util.Optional;
5 | import org.springframework.data.jpa.repository.JpaRepository;
6 | import org.springframework.data.jpa.repository.Query;
7 | import org.springframework.stereotype.Repository;
8 | import haengdong.event.domain.event.Event;
9 |
10 | @Repository
11 | public interface BillRepository extends JpaRepository {
12 |
13 | @Query("""
14 | select b
15 | from Bill b
16 | join fetch b.billDetails bd
17 | join fetch bd.eventMember
18 | where b.event = :event
19 | """)
20 | List findAllByEvent(Event event);
21 |
22 | Optional findFirstByEventOrderByIdDesc(Event event);
23 | }
24 |
--------------------------------------------------------------------------------
/client/jest.setup.ts:
--------------------------------------------------------------------------------
1 | import {server} from './src/mocks/server';
2 | import * as router from 'react-router';
3 | import '@testing-library/jest-dom'; // toBeInTheDocument를 인식하기 위해 @testing-library/jest-dom/extend-expect추가
4 | import 'jest-canvas-mock'; // jsdom은 canvas를 추가할 수 없기 때문에 이를 해결하는 라이브러리 추가
5 |
6 | beforeAll(() => {
7 | server.listen();
8 |
9 | Object.defineProperty(window, 'location', {
10 | writable: true,
11 | value: {
12 | ...window.location,
13 | pathname: '/event/abc-123/', // 원하는 pathname 설정
14 | },
15 | });
16 | });
17 | afterEach(() => server.resetHandlers());
18 | afterAll(() => server.close());
19 |
20 | beforeAll(() => {});
21 | jest.mock('./src/utils/captureError');
22 | jest.mock('./src/utils/sendLogToSentry');
23 |
24 | jest.spyOn(router, 'useNavigate').mockImplementation(() => jest.fn());
25 |
--------------------------------------------------------------------------------
/client/src/hooks/queries/images/useRequestDeleteImages.ts:
--------------------------------------------------------------------------------
1 | import {useMutation, useQueryClient} from '@tanstack/react-query';
2 |
3 | import {requestDeleteImage, RequestDeleteImage} from '@apis/request/images';
4 |
5 | import getEventIdByUrl from '@utils/getEventIdByUrl';
6 |
7 | import QUERY_KEYS from '@constants/queryKeys';
8 |
9 | const useRequestDeleteImage = () => {
10 | const eventId = getEventIdByUrl();
11 | const queryClient = useQueryClient();
12 |
13 | const {mutate, ...rest} = useMutation({
14 | mutationFn: ({imageId}: RequestDeleteImage) => requestDeleteImage({eventId, imageId}),
15 | onSuccess: () => {
16 | queryClient.invalidateQueries({queryKey: [QUERY_KEYS.images, eventId]});
17 | },
18 | });
19 |
20 | return {deleteImage: mutate, ...rest};
21 | };
22 |
23 | export default useRequestDeleteImage;
24 |
--------------------------------------------------------------------------------
/client/src/hooks/queries/images/useRequestPostImages.ts:
--------------------------------------------------------------------------------
1 | import {useMutation, useQueryClient} from '@tanstack/react-query';
2 |
3 | import {requestPostImages, RequestPostImages} from '@apis/request/images';
4 |
5 | import getEventIdByUrl from '@utils/getEventIdByUrl';
6 |
7 | import QUERY_KEYS from '@constants/queryKeys';
8 |
9 | const useRequestPostImages = () => {
10 | const eventId = getEventIdByUrl();
11 | const queryClient = useQueryClient();
12 |
13 | const {mutateAsync, ...rest} = useMutation({
14 | mutationFn: ({formData}: RequestPostImages) => requestPostImages({eventId, formData}),
15 | onSuccess: () => {
16 | queryClient.removeQueries({queryKey: [QUERY_KEYS.images, eventId]});
17 | },
18 | });
19 |
20 | return {postImages: mutateAsync, ...rest};
21 | };
22 |
23 | export default useRequestPostImages;
24 |
--------------------------------------------------------------------------------
/client/src/components/Design/components/ContentLabel/ContentLabel.stories.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import type {Meta, StoryObj} from '@storybook/react';
3 |
4 | import ContentLabel from './ContentLabel';
5 |
6 | const meta = {
7 | title: 'Components/ContentLabel',
8 | component: ContentLabel,
9 | tags: ['autodocs'],
10 | parameters: {
11 | layout: 'centered',
12 | },
13 | decorators: [
14 | Story => (
15 |
16 |
17 |
18 | ),
19 | ],
20 | argTypes: {},
21 | args: {
22 | children: '기본 계좌번호',
23 | onClick: () => {},
24 | },
25 | } satisfies Meta;
26 |
27 | export default meta;
28 |
29 | type Story = StoryObj;
30 |
31 | export const Playground: Story = {};
32 |
--------------------------------------------------------------------------------
/client/src/components/Design/components/TopNav/TopNav.tsx:
--------------------------------------------------------------------------------
1 | import {ReactNode} from 'react';
2 |
3 | import {topNavStyle, topNavWrapperStyle} from './TopNav.style';
4 | import NavText from './NavText';
5 | import NavIcon from './NavIcon';
6 |
7 | type TopNavProps = {
8 | left?: ReactNode;
9 | right?: ReactNode;
10 | };
11 |
12 | const TopNav = ({left, right}: TopNavProps) => {
13 | return (
14 |
20 | );
21 | };
22 |
23 | /**
24 | * onClick를 넘겨주었으면 routePath는 무시됩니다.
25 | */
26 | TopNav.Text = NavText;
27 |
28 | /**
29 | * onClick를 넘겨주었으면 routePath는 무시됩니다.
30 | */
31 | TopNav.Icon = NavIcon;
32 |
33 | export default TopNav;
34 |
--------------------------------------------------------------------------------
/client/src/components/Design/components/Title/Title.style.ts:
--------------------------------------------------------------------------------
1 | import {css} from '@emotion/react';
2 |
3 | import {Theme} from '@theme/theme.type';
4 |
5 | export const titleStyle = (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',
13 | borderRadius: '0.75rem',
14 | });
15 |
16 | export const titleContainerStyle = (hasDropdown: boolean) =>
17 | css({
18 | display: 'flex',
19 | justifyContent: 'space-between',
20 | paddingLeft: '0.5rem',
21 | paddingRight: hasDropdown ? '0' : '0.5rem',
22 | });
23 |
24 | export const amountContainerStyle = css({
25 | display: 'flex',
26 | justifyContent: 'space-between',
27 | alignItems: 'end',
28 | paddingInline: '0.5rem',
29 | });
30 |
--------------------------------------------------------------------------------
/client/src/components/Design/components/Top/Top.tsx:
--------------------------------------------------------------------------------
1 | /** @jsxImportSource @emotion/react */
2 | import {css} from '@emotion/react';
3 | import React from 'react';
4 |
5 | import Line from './Line';
6 | import EditableLine from './EditableLine';
7 |
8 | Top.Line = Line;
9 | Top.EditableLine = EditableLine;
10 |
11 | export default function Top({children}: React.PropsWithChildren) {
12 | const childrenTexts: string[] = [];
13 | React.Children.forEach(children, child => {
14 | if (React.isValidElement(child) && child.type === Top.Line) {
15 | childrenTexts.push(child.props.text);
16 | }
17 | });
18 |
19 | return (
20 |
27 | {children}
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/client/src/hooks/queries/event/useRequestPostGuestEvent.ts:
--------------------------------------------------------------------------------
1 | import {useMutation, useQueryClient} from '@tanstack/react-query';
2 |
3 | import {requestPostGuestEvent} from '@apis/request/event';
4 | import {EventCreationData} from 'types/serviceType';
5 |
6 | const useRequestPostGuestEvent = () => {
7 | const queryClient = useQueryClient();
8 |
9 | const {mutateAsync, ...rest} = useMutation({
10 | mutationFn: ({eventName, nickname, password}: EventCreationData) =>
11 | requestPostGuestEvent({eventName, nickname, password}),
12 | onSuccess: () => {
13 | queryClient.removeQueries();
14 | },
15 | });
16 |
17 | // 실행 순서를 await으로 보장하기 위해 mutateAsync 사용
18 | return {
19 | postEvent: mutateAsync,
20 | isPostEventPending: rest.isPending,
21 | ...rest,
22 | };
23 | };
24 |
25 | export default useRequestPostGuestEvent;
26 |
--------------------------------------------------------------------------------
/client/src/components/Design/components/NumberKeyboard/keypads.ts:
--------------------------------------------------------------------------------
1 | export type KeyboardType = 'number' | 'string' | 'amount';
2 |
3 | type Keypad = {
4 | keypad: string;
5 | ariaLabel: string;
6 | };
7 |
8 | export const makeKeypads = (type: KeyboardType): Keypad[] => {
9 | const keypads: Keypad[] = [];
10 |
11 | // 1~9는 동일
12 | keypads.push(...new Array(9).fill(0).map((_, index) => ({keypad: String(index + 1), ariaLabel: String(index + 1)})));
13 |
14 | // amount는 00버튼 나머지는 숨기기
15 | if (type === 'amount') {
16 | keypads.push({keypad: '00', ariaLabel: '0 2개 버튼'});
17 | } else {
18 | keypads.push({keypad: '', ariaLabel: ''});
19 | }
20 |
21 | // 나머지 0과 지우기 버튼
22 | keypads.push({keypad: '0', ariaLabel: '0'});
23 | keypads.push({
24 | keypad: '<-',
25 | ariaLabel: '마지막 숫자 지우기',
26 | });
27 |
28 | return keypads;
29 | };
30 |
--------------------------------------------------------------------------------
/client/src/components/Design/components/Container/Container.style.ts:
--------------------------------------------------------------------------------
1 | import {css} from '@emotion/react';
2 |
3 | import {attributeWithUnit} from '@components/Design/utils/attribute';
4 |
5 | import {ContainerProps} from './Container.type';
6 |
7 | export const containerStyle = ({maxW, p, m, br, b, bg, center}: ContainerProps) => {
8 | const [maxWidthValue, paddingValue, marginValue, borderRadiusValue, borderValue] = attributeWithUnit({
9 | maxW,
10 | p,
11 | m,
12 | br,
13 | b,
14 | });
15 |
16 | return css`
17 | width: 100%;
18 | ${center && 'display: flex; justify-content: center; align-items: center;'}
19 | max-width: ${maxWidthValue};
20 | background: ${bg};
21 | padding: ${paddingValue};
22 | margin: ${marginValue};
23 | border-radius: ${borderRadiusValue};
24 | border: ${borderValue};
25 | `;
26 | };
27 |
--------------------------------------------------------------------------------
/server/src/main/java/haengdong/event/presentation/response/EventDetailResponse.java:
--------------------------------------------------------------------------------
1 | package haengdong.event.presentation.response;
2 |
3 | import haengdong.event.application.response.EventDetailAppResponse;
4 | import haengdong.user.domain.AccountNumber;
5 | import haengdong.user.domain.Bank;
6 |
7 | public record EventDetailResponse(
8 | String eventName,
9 | String bankName,
10 | String accountNumber,
11 | Boolean createdByGuest
12 | ) {
13 |
14 | public static EventDetailResponse of(EventDetailAppResponse response) {
15 | Bank bank = response.bankName();
16 | AccountNumber accountNumber = response.accountNumber();
17 | return new EventDetailResponse(response.eventName(), bank == null ? "" : bank.getName(), accountNumber == null ? "" : accountNumber.getValue(), response.createdByGuest());
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/client/src/components/Design/components/CreatedEventItem/CreatedEventItem.style.ts:
--------------------------------------------------------------------------------
1 | import {css} from '@emotion/react';
2 |
3 | import {WithTheme} from '@components/Design/type/withTheme';
4 |
5 | export const inProgressCheckStyle = ({inProgress, theme}: WithTheme<{inProgress: boolean}>) =>
6 | css({
7 | display: 'flex',
8 | alignItems: 'center',
9 | gap: '0.125rem',
10 | border: `1px solid ${inProgress ? theme.colors.primary : theme.colors.gray}`,
11 | borderRadius: '0.5rem',
12 | padding: '0.25rem 0.375rem',
13 | height: '1.25rem',
14 |
15 | '.in-progress-check-text': {
16 | color: inProgress ? theme.colors.primary : theme.colors.gray,
17 | paddingTop: '0.0625rem',
18 | },
19 | });
20 |
21 | export const touchAreaStyle = css({
22 | position: 'relative',
23 | overflow: 'hidden',
24 |
25 | width: '100%',
26 | });
27 |
--------------------------------------------------------------------------------
/client/src/assets/image/error-circle.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------