Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
8e5f0e2
hotfix: ios일 때 알림 탭이 보이지 않도록 수정
JeLee-river Mar 10, 2026
cf5beb4
hotfix: 웹일때 알림 설정이 보이는 문제 수정
JeLee-river Mar 10, 2026
c8a391c
Merge remote-tracking branch 'origin/dev'
JeLee-river Mar 10, 2026
32f60d3
hotfix: ios일때 알림 설정 페이지로 접근하지 못하도록 수정
JeLee-river Mar 12, 2026
6e094a5
Merge remote-tracking branch 'origin/dev'
JeLee-river Mar 13, 2026
14de406
hotfix: ios에서 알림 설정 탭이 보이지 않는 문제 수정
JeLee-river Mar 13, 2026
87dc4ef
Merge remote-tracking branch 'origin/dev'
JeLee-river Mar 17, 2026
91fab52
Merge branch 'dev'
Db0111 Mar 25, 2026
33c7818
Merge branch 'dev'
Db0111 Mar 25, 2026
549bf78
[BOM-1042] refactor: 랜딩페이지 인덱싱 정책 수정 (#173)
JeLee-river Mar 25, 2026
4b105e1
Merge branch 'dev'
Db0111 Mar 27, 2026
8a80c71
hotfix: 회원가입 페이지에서 profile api가 호출되지 않도록 수정
JeLee-river Mar 27, 2026
59dfcd4
Merge remote-tracking branch 'origin/dev'
JeLee-river Apr 1, 2026
f906d17
Revert "[BOM-1053] fix: 안드로이드 highlight toolbar 에러 해결 (#180)"
JeLee-river Apr 1, 2026
5d6b167
hotfix: 카테고리 알림이 토글되지 않는 문제 수정
JeLee-river Apr 1, 2026
d91769b
Reapply "[BOM-1053] fix: 안드로이드 highlight toolbar 에러 해결 (#180)"
JeLee-river Apr 4, 2026
39525db
[BOM-1066] nanumgothic으로 폰트 변경 (#188)
Db0111 Apr 10, 2026
f912816
Merge remote-tracking branch 'origin/dev'
JeLee-river Apr 16, 2026
ad60768
Merge remote-tracking branch 'origin/dev'
JeLee-river Apr 22, 2026
8bba808
Merge branch 'dev'
jaeyoung-kwon Apr 22, 2026
d755bc4
Merge remote-tracking branch 'origin/dev'
JeLee-river Apr 26, 2026
55e0ba1
Merge branch 'dev'
jaeyoung-kwon Apr 27, 2026
03b23e1
chore: trigger frontend deploy
JeLee-river Apr 29, 2026
c420931
chore: trigger maeil-mail deploy
JeLee-river Apr 29, 2026
c9f509d
Merge branch 'dev'
jaeyoung-kwon Apr 30, 2026
b670bae
Merge branch 'dev'
jaeyoung-kwon Apr 30, 2026
96045e2
Merge remote-tracking branch 'origin/dev'
jaeyoung-kwon Apr 30, 2026
078cca7
Merge branch 'dev'
jaeyoung-kwon May 1, 2026
f1ae627
Merge remote-tracking branch 'origin/dev'
JeLee-river May 14, 2026
f12083f
Merge remote-tracking branch 'origin/dev'
JeLee-river May 15, 2026
8083721
Merge branch 'dev'
jaeyoung-kwon May 18, 2026
2f51475
Merge branch 'dev'
jaeyoung-kwon May 18, 2026
a7be387
Merge branch 'dev'
jaeyoung-kwon May 23, 2026
3eeab81
Merge remote-tracking branch 'origin/dev'
JeLee-river May 27, 2026
59dcab7
Merge remote-tracking branch 'origin/dev'
JeLee-river May 27, 2026
e46fd8e
Merge remote-tracking branch 'origin/dev'
JeLee-river May 27, 2026
5a260ee
Merge remote-tracking branch 'origin/dev'
JeLee-river May 28, 2026
e0fc6b7
Merge remote-tracking branch 'origin/dev'
JeLee-river May 28, 2026
d8f1519
Merge remote-tracking branch 'origin/dev'
JeLee-river Jun 12, 2026
4164e5c
feat: Flyway 뷰어 UI 개선 — 순서역전 표시 / dev·prod 파이프라인 (#255)
Ryan-Dia Jun 16, 2026
5e0b351
Revert "feat: Flyway 뷰어 UI 개선 — 순서역전 표시 / dev·prod 파이프라인 (#255)" (#257)
Ryan-Dia Jun 16, 2026
a6f9052
hotfix: 매일메일에서 회원가입 후 페이지로 돌아가지 못하는 문제 수정
JeLee-river Jun 30, 2026
7f4a8f6
chore: 연속 읽기 보호막 UI main 체리픽 반영 (#274)
sanghee01 Jun 30, 2026
ac27786
fix: 스토리지 차단 환경에서 랜딩 리다이렉트 크래시 방어
jaeyoung-kwon Jul 1, 2026
4ad76f2
Merge remote-tracking branch 'origin/dev' into fix/safe-localstorage-…
JeLee-river Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions maeil-mail/src/constants/urls.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const MAEIL_MAIL_URL = 'https://maeilmail.bombom.news';
export const WEB_URL = 'https://bombom.news';
24 changes: 21 additions & 3 deletions maeil-mail/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root';
import { Route as SignupRouteImport } from './routes/signup';
import { Route as IndexRouteImport } from './routes/index';
import { Route as ContentsContentIdAnswerRouteImport } from './routes/contents/$contentId/answer';

const SignupRoute = SignupRouteImport.update({
id: '/signup',
path: '/signup',
getParentRoute: () => rootRouteImport,
} as any);
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
Expand All @@ -25,32 +31,43 @@ const ContentsContentIdAnswerRoute = ContentsContentIdAnswerRouteImport.update({

export interface FileRoutesByFullPath {
'/': typeof IndexRoute;
'/signup': typeof SignupRoute;
'/contents/$contentId/answer': typeof ContentsContentIdAnswerRoute;
}
export interface FileRoutesByTo {
'/': typeof IndexRoute;
'/signup': typeof SignupRoute;
'/contents/$contentId/answer': typeof ContentsContentIdAnswerRoute;
}
export interface FileRoutesById {
__root__: typeof rootRouteImport;
'/': typeof IndexRoute;
'/signup': typeof SignupRoute;
'/contents/$contentId/answer': typeof ContentsContentIdAnswerRoute;
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths: '/' | '/contents/$contentId/answer';
fullPaths: '/' | '/signup' | '/contents/$contentId/answer';
fileRoutesByTo: FileRoutesByTo;
to: '/' | '/contents/$contentId/answer';
id: '__root__' | '/' | '/contents/$contentId/answer';
to: '/' | '/signup' | '/contents/$contentId/answer';
id: '__root__' | '/' | '/signup' | '/contents/$contentId/answer';
fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute;
SignupRoute: typeof SignupRoute;
ContentsContentIdAnswerRoute: typeof ContentsContentIdAnswerRoute;
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/signup': {
id: '/signup';
path: '/signup';
fullPath: '/signup';
preLoaderRoute: typeof SignupRouteImport;
parentRoute: typeof rootRouteImport;
};
'/': {
id: '/';
path: '/';
Expand All @@ -70,6 +87,7 @@ declare module '@tanstack/react-router' {

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
SignupRoute: SignupRoute,
ContentsContentIdAnswerRoute: ContentsContentIdAnswerRoute,
};
export const routeTree = rootRouteImport
Expand Down
27 changes: 27 additions & 0 deletions maeil-mail/src/routes/signup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createFileRoute, useSearch } from '@tanstack/react-router';
import { useEffect } from 'react';
import { WEB_URL } from '@/constants/urls';

export const Route = createFileRoute('/signup')({
head: () => ({
meta: [{ name: 'robots', content: 'noindex, nofollow' }],
}),
component: RouteComponent,
validateSearch: (search: { email?: string; name?: string }) => ({
email: search.email,
name: search.name,
}),
});

function RouteComponent() {
const { email, name } = useSearch({ from: '/signup' });

useEffect(() => {
const params = new URLSearchParams();
if (email) params.set('email', email);
if (name) params.set('name', name);
window.location.replace(`${WEB_URL}/signup?${params.toString()}`);
}, [email, name]);

return null;
}
1 change: 1 addition & 0 deletions web/src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ const router = createRouter({
},
scrollRestoration: true,
defaultNotFoundComponent: NotFound,
defaultErrorComponent: PageErrorFallback,
});

if (isProduction) {
Expand Down
7 changes: 7 additions & 0 deletions web/src/pages/landing/constants/localStorage.ts
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
import { createStorage } from '@/utils/localStorage';

export const LANDING_VISITED_KEY = 'hasVisitedLanding';

export const landingVisitedStorage = createStorage<boolean>(
LANDING_VISITED_KEY,
false,
);
4 changes: 2 additions & 2 deletions web/src/routes/landing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import LandingHeader from '@/pages/landing/components/LandingHeader';
import LandingHero from '@/pages/landing/components/LandingHero';
import LandingPopularNewsletters from '@/pages/landing/components/LandingPopularNewsletters';
import PainPoint from '@/pages/landing/components/PainPoint';
import { LANDING_VISITED_KEY } from '@/pages/landing/constants/localStorage';
import { landingVisitedStorage } from '@/pages/landing/constants/localStorage';
import type { Device } from '@/hooks/useDevice';

export const Route = createFileRoute('/landing')({
Expand All @@ -32,7 +32,7 @@ function LandingPage() {
const device = useDevice();

useEffect(() => {
localStorage.setItem(LANDING_VISITED_KEY, 'true');
landingVisitedStorage.set(true);
}, []);

return (
Expand Down
25 changes: 25 additions & 0 deletions web/src/utils/localStorage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,29 @@ describe('createStorage', () => {
expect(window.localStorage.removeItem).toHaveBeenCalledWith(KEY);
expect(window.localStorage.getItem(KEY)).toBeNull();
});

it('get: storage 접근이 예외를 던지면 defaultData를 반환한다.', () => {
jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
throw new DOMException('Access is denied.', 'SecurityError');
});

expect(() => storage.get()).not.toThrow();
expect(storage.get()).toEqual(DEFAULT_DATA);
});

it('set: storage 접근이 예외를 던져도 throw하지 않는다.', () => {
jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
throw new DOMException('Access is denied.', 'SecurityError');
});

expect(() => storage.set({ count: 1 })).not.toThrow();
});

it('remove: storage 접근이 예외를 던져도 throw하지 않는다.', () => {
jest.spyOn(Storage.prototype, 'removeItem').mockImplementation(() => {
throw new DOMException('Access is denied.', 'SecurityError');
});

expect(() => storage.remove()).not.toThrow();
});
});
23 changes: 16 additions & 7 deletions web/src/utils/localStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,32 @@ interface StorageType<T extends Serializable> {
remove: () => void;
}

const storage = window.localStorage;

export const createStorage = <T extends Serializable>(
key: string,
defaultData?: T,
): StorageType<T> => ({
get() {
const data = storage.getItem(key);
return data ? JSON.parse(data) : defaultData;
try {
const data = window.localStorage.getItem(key);
return data ? JSON.parse(data) : defaultData;
} catch {
return defaultData;
}
},

set(data) {
const stringifyData = JSON.stringify(data);
storage.setItem(key, stringifyData);
try {
window.localStorage.setItem(key, JSON.stringify(data));
} catch {
// noop
}
},

remove() {
storage.removeItem(key);
try {
window.localStorage.removeItem(key);
} catch {
// noop
}
},
});
Loading