-
-
-
-
+
+
+
+
+
+ 부크(Vook)
+
+ {' | '}
+
+ 이메일 vook.help@gmail.com
+
+
+
diff --git a/apps/web/src/components/layout/Layout.css.ts b/apps/web/src/components/layout/Layout.css.ts
index ab2695c..ef44ea4 100644
--- a/apps/web/src/components/layout/Layout.css.ts
+++ b/apps/web/src/components/layout/Layout.css.ts
@@ -1,6 +1,8 @@
import { style } from '@vanilla-extract/css'
import { vars } from '@vook-client/design-system'
+import { FOOTER_HEIGHT } from '@/styles/layout'
+
export const flexCenter = style({
display: 'flex',
justifyContent: 'center',
@@ -40,20 +42,29 @@ export const footer = style([
position: 'absolute',
bottom: 0,
left: 0,
- height: 257,
+ height: FOOTER_HEIGHT,
width: '100%',
backgroundColor: vars.colors['component-normal'],
- padding: '40px 0',
- zIndex: -10,
+ padding: '58px 0',
+ zIndex: 100,
},
])
+export const footerEmail = style({
+ opacity: 0.5,
+})
+
export const footerIconContainer = style({
position: 'absolute',
top: 40,
left: 0,
})
+export const footerPolicy = style({
+ display: 'flex',
+ gap: 24,
+})
+
export const footerContainer = style({
display: 'flex',
flexDirection: 'column',
@@ -62,6 +73,11 @@ export const footerContainer = style({
zIndex: -10,
})
+export const footerRow = style({
+ display: 'flex',
+ justifyContent: 'space-between',
+})
+
export const footerLine = style({
borderColor: vars.colors['semantic-line-normal'],
})
diff --git a/apps/web/src/middleware.ts b/apps/web/src/middleware.ts
index e97abc2..3302371 100644
--- a/apps/web/src/middleware.ts
+++ b/apps/web/src/middleware.ts
@@ -112,7 +112,12 @@ const checkUserStatusMiddleware =
return finalResponse
}
-const onlyRegisteredMatch = ['/onboarding', '/user/edit']
+const onlyRegisteredMatch = [
+ '/onboarding',
+ '/user/edit',
+ '/workspace',
+ '/vocabulary/',
+]
const onlyRegisteredMiddleware = checkUserStatusMiddleware([
UserStatus.Registered,
@@ -135,7 +140,6 @@ export async function middleware(req: NextRequest) {
if (
onlyRegisteredMatch.some((url) => req.nextUrl.pathname.includes(url)) ||
- req.nextUrl.pathname === '/' ||
req.nextUrl.pathname.includes('/vocabulary/')
) {
return onlyRegisteredMiddleware(req, response, '/login')
diff --git a/apps/web/src/styles/layout.ts b/apps/web/src/styles/layout.ts
index 4d2246b..e92e338 100644
--- a/apps/web/src/styles/layout.ts
+++ b/apps/web/src/styles/layout.ts
@@ -1,3 +1,5 @@
export const SIDE_BAR_WIDTH = 260
export const HEADER_HEIGHT = 86
+
+export const FOOTER_HEIGHT = 208
diff --git a/apps/web/src/styles/motion.ts b/apps/web/src/styles/motion.ts
new file mode 100644
index 0000000..89d856f
--- /dev/null
+++ b/apps/web/src/styles/motion.ts
@@ -0,0 +1,76 @@
+import { AnimationProps, MotionProps } from 'framer-motion'
+import { HTMLAttributes } from 'react'
+
+const springTransition = {
+ type: 'spring',
+ stiffness: 100,
+}
+
+const defaultProperties = {
+ initial: 'hidden',
+ animate: 'visible',
+}
+
+export const fromLeft = {
+ hidden: { opacity: 0, x: -30 },
+ visible: {
+ opacity: 1,
+ x: 0,
+ transition: springTransition,
+ },
+}
+
+export const fromBottom = {
+ hidden: { opacity: 0, y: 30 },
+ visible: {
+ opacity: 1,
+ y: 0,
+ transition: springTransition,
+ },
+}
+
+export const exitToBottom = {
+ opacity: 0,
+ y: 30,
+ transition: {
+ duration: 0.1,
+ },
+}
+
+export const appearFromLeft: AnimationProps = {
+ ...defaultProperties,
+ variants: fromLeft,
+}
+
+export const appearFromBottom: AnimationProps = {
+ ...defaultProperties,
+ variants: fromBottom,
+ exit: exitToBottom,
+}
+
+export const orchestrate: AnimationProps = {
+ ...defaultProperties,
+ variants: {
+ visible: {
+ transition: {
+ when: 'beforeChildren',
+ staggerChildren: 0.4,
+ },
+ },
+ },
+}
+
+export const orchestrateFast: AnimationProps = {
+ ...defaultProperties,
+ variants: {
+ visible: {
+ transition: {
+ when: 'beforeChildren',
+ staggerChildren: 0.05,
+ },
+ },
+ },
+}
+
+export type MotionComponentProps = HTMLAttributes
&
+ MotionProps & { isOrchestration?: boolean }
diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json
index 00c41b0..341a72b 100644
--- a/apps/web/tsconfig.json
+++ b/apps/web/tsconfig.json
@@ -16,6 +16,7 @@
"@/mock/*": ["./src/mock/*"],
"@/modals/*": ["./src/modals/*"],
"@/toasters/*": ["./src/toasters/*"],
+ "@/public/*": ["./public/*"],
"@/store/*": ["./src/store/*"]
},
"types": ["chrome"]
diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts
index 49d6107..113b753 100644
--- a/packages/api/src/index.ts
+++ b/packages/api/src/index.ts
@@ -52,6 +52,7 @@ export type {
GetTermResponse,
Terms,
TermSort,
+ TermSortValues,
} from './services/term/model'
export { termSort } from './services/term/model'
export {
diff --git a/packages/api/src/services/search/model.ts b/packages/api/src/services/search/model.ts
index ba3699f..9a18fe6 100644
--- a/packages/api/src/services/search/model.ts
+++ b/packages/api/src/services/search/model.ts
@@ -14,7 +14,7 @@ export type SearchSort = (typeof searchSort)[keyof typeof searchSort]
export interface SearchDTO {
vocabularyUids: string[]
- query: string
+ queries: string[]
withFormat?: boolean
highlightPreTag?: string
highlightPostTag?: string
@@ -27,13 +27,15 @@ export interface SearchHit {
meaning: string
}
+export interface Record {
+ vocabularyUid: string
+ hits: SearchHit[]
+}
+
export interface SearchResponse {
code: string
result: {
query: string
- records: {
- vocabularyUid: string
- hits: SearchHit[]
- }[]
+ records: Record[]
}
}
diff --git a/packages/api/src/services/search/searchService.ts b/packages/api/src/services/search/searchService.ts
index b485080..b9bad68 100644
--- a/packages/api/src/services/search/searchService.ts
+++ b/packages/api/src/services/search/searchService.ts
@@ -1,16 +1,74 @@
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+
import { QueryClient } from '@tanstack/react-query'
import { APIBuilder } from '../../lib/fetcher'
-import { SearchDTO, SearchResponse } from './model'
+import { SearchDTO, SearchHit, SearchResponse } from './model'
+
+const groupByVocabularyUid = (response: SearchResponse): SearchResponse => {
+ const grouped: SearchResponse = {
+ code: response.code,
+ result: {
+ ...response.result,
+ records: [],
+ },
+ }
+
+ const records: SearchResponse['result']['records'] = []
+
+ const hitIds = new Set()
+
+ response.result.records.forEach((record) => {
+ const vocabularyUid = record.vocabularyUid
+
+ const existingRecord = records.find(
+ (r) => r.vocabularyUid === vocabularyUid,
+ )
+
+ if (existingRecord) {
+ const uniqueHits: SearchHit[] = []
+
+ record.hits.forEach((hit) => {
+ if (!hitIds.has(hit.uid)) {
+ hitIds.add(hit.uid)
+ uniqueHits.push(hit)
+ }
+ })
+
+ existingRecord.hits.push(...uniqueHits)
+ } else {
+ const uniqueHits: SearchHit[] = []
+
+ record.hits.forEach((hit) => {
+ if (!hitIds.has(hit.uid)) {
+ hitIds.add(hit.uid)
+ uniqueHits.push(hit)
+ }
+ })
+
+ records.push({
+ vocabularyUid,
+ hits: uniqueHits,
+ })
+ }
+ })
+
+ grouped.result.records = records
+
+ return grouped
+}
export const searchService = {
async search(dto: SearchDTO, client: QueryClient) {
- return APIBuilder.post('/terms/search')
+ const result = await APIBuilder.post('/terms/search')
.withCredentials(client)
.build()
.call({
body: JSON.stringify(dto),
})
+
+ return groupByVocabularyUid(result)
},
}
diff --git a/packages/design-system/src/components/TypoLogo/TypoLogo.tsx b/packages/design-system/src/components/TypoLogo/TypoLogo.tsx
index 88cbcad..241b4d5 100644
--- a/packages/design-system/src/components/TypoLogo/TypoLogo.tsx
+++ b/packages/design-system/src/components/TypoLogo/TypoLogo.tsx
@@ -1,4 +1,4 @@
-type TypoLogoSizes = 'big' | 'small'
+type TypoLogoSizes = 'big' | 'small' | 'gray'
const typoLogos: {
[key in TypoLogoSizes]: JSX.Element
@@ -65,6 +65,48 @@ const typoLogos: {
/>
),
+ gray: (
+
+ ),
}
export interface TypoLogoProps {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9088f3e..035c270 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -156,6 +156,9 @@ importers:
dayjs:
specifier: ^1.11.11
version: 1.11.11
+ framer-motion:
+ specifier: ^11.3.2
+ version: 11.3.2(react-dom@18.3.1)(react@18.3.1)
js-cookie:
specifier: ^3.0.5
version: 3.0.5
@@ -11571,6 +11574,25 @@ packages:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
dev: true
+ /framer-motion@11.3.2(react-dom@18.3.1)(react@18.3.1):
+ resolution: {integrity: sha512-RgjSzrNFZmedWcvmW4MMc84A7UcoY37jocadE3Mbg3o+UMofodfyeNnYD/HR15UhP22/bb5KOebNhYOj4mYkpQ==}
+ peerDependencies:
+ '@emotion/is-prop-valid': '*'
+ react: ^18.0.0
+ react-dom: ^18.0.0
+ peerDependenciesMeta:
+ '@emotion/is-prop-valid':
+ optional: true
+ react:
+ optional: true
+ react-dom:
+ optional: true
+ dependencies:
+ react: 18.3.1
+ react-dom: 18.3.1(react@18.3.1)
+ tslib: 2.6.2
+ dev: false
+
/fresh@0.5.2:
resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==}
engines: {node: '>= 0.6'}