Skip to content

jellybrown/bread-map-admin-web

Β 
Β 

Repository files navigation

λŒ€λ™λΉ΅μ§€λ„ μ–΄λ“œλ―Ό

1. μ‹€ν–‰ 방법

1. yarn (install)
2. yarn run dev
  • ν™˜κ²½λ³€μˆ˜ νŒŒμΌμ€ μŠ¬λž™ or 직접 jellybrownμ—κ²Œ λ¬Όμ–΄λ΄μ£Όμ„Έμš” πŸ™‚

2. ν”„λ‘œμ νŠΈ 관리 κ°€μ΄λ“œ

  • ν”„λ‘œμ νŠΈλŠ” ν˜„μž¬ dev ν™˜κ²½μ—μ„œλ§Œ κ°œλ°œμ€‘μž…λ‹ˆλ‹€. (dev = master 브랜치)
  1. 본인의 κΉƒν—™ κ²Œμ •μœΌλ‘œ fork
  2. λŒ€λ™λΉ΅μ§€λ„ 레포 (upstream) - 본인 레포 (origin) 둜 μ„€μ •ν•œ ν›„ κ΄€λ¦¬ν•΄μ£Όμ„Έμš”.
  3. 개발 ν›„ originμ—μ„œ upstream(master)으둜 PR을 λ‚ λ¦½λ‹ˆλ‹€.
  4. 자유둭게 μ½”λ“œλ¦¬λ·°, κ΅¬μ‘°κ°œμ„  λ“± 말씀해주셔도 λ©λ‹ˆλ‹€!

3. 라이브러리

UI

  • Emotion

μƒνƒœκ΄€λ¦¬

  • Client: Redux Toolkit
  • Server: React Query

μ½”λ“œ μ»¨λ²€μ…˜

  • Eslint (airbnb, μ›Ή μ ‘κ·Όμ„±, hooks)
  • Prettier [WebStorm μ„€μ • κ°€μ΄λ“œ]

ν…ŒμŠ€νŠΈ

  • React Testing Library
  • Jest

4. ν”„λ‘œμ νŠΈ ꡬ쑰

β”œβ”€β”€ apis
β”‚   β”œβ”€β”€ axios # instance μ„ μ–Έ, interceptor 둜직 
β”‚   β”œβ”€β”€ bakery # api 뢄리
β”‚       β”œβ”€β”€ bakery.ts # μ‚¬μš©λ˜λŠ” api class 
β”‚       β”œβ”€β”€ bakeryClient.ts # api class에 μ£Όμž…ν•΄μ„œ μ‚¬μš©ν•˜λŠ” client
β”‚       └── useBakery.ts # Query hooks
β”‚   └── ...
β”œβ”€β”€ components
β”‚   β”œβ”€β”€ __tests__
β”‚       β”œβ”€β”€ SharedTable.test.tsx # Shared (곡톡) μ»΄ν¬λ„ŒνŠΈ ν…ŒμŠ€νŠΈ
β”‚       └── LoginForm.test.tsx # 일반 μ»΄ν¬λ„ŒνŠΈ ν…ŒμŠ€νŠΈ
β”‚   β”œβ”€β”€ Auth # 인증 κ΄€λ ¨ μ»΄ν¬λ„ŒνŠΈ
β”‚   β”œβ”€β”€ Shared # 곡톡 μ»΄ν¬λ„ŒνŠΈ
β”‚   └── ...
β”œβ”€β”€ constants # μƒμˆ˜κ΄€λ¦¬
β”œβ”€β”€ containers # 화면에 ν•΄λ‹Ήν•˜λŠ” μ»΄ν¬λ„ŒνŠΈλ₯Ό λ‹΄κ³ μžˆλŠ” μ»¨ν…Œμ΄λ„ˆ. ν•˜μœ„ν΄λ”λŠ” ν™”λ©΄ κΈ°μ€€ 뢄리
β”œβ”€β”€ context # μ „μ—­/μ»΄ν¬λ„ŒνŠΈμƒνƒœ 관리가 ν•„μš”ν•œ 경우 μ‚¬μš©
β”œβ”€β”€ hooks # Custom hooks μ •μ˜
β”œβ”€β”€ routes
β”‚   β”œβ”€β”€ loader.ts # 라우트 μ§„μž… μ „ 싀행이 ν•„μš”ν•œ 경우 μ‚¬μš© 
β”‚   └── ...
β”œβ”€β”€ store # ν˜„μž¬ bakery form에 λŒ€ν•œ μƒνƒœλ₯Ό μ €μž₯ν•˜κ³  μžˆμœΌλ‚˜, λ‹€λ₯Έκ±Έλ‘œ λ³€κ²½μ˜ˆμ •
β”œβ”€β”€ styles
β”‚   β”œβ”€β”€ common # 자주 μ‚¬μš©λ˜λŠ” μŠ€νƒ€μΌ
β”‚   β”œβ”€β”€ global.tsx # css μ΄ˆκΈ°ν™” 및 κΈ°λ³Έμ„€μ • 
β”‚   β”œβ”€β”€ theme.ts # ν”„λ‘œμ νŠΈ ν…Œλ§ˆ 색상/크기 λ“±
β”‚   └── ...
β”œβ”€β”€ tests # ν…ŒμŠ€νŠΈμ‹œ ν•„μš”ν•œ mock, function ..
└── utils # 독립적인 ν•¨μˆ˜λ“€ μ„ μ–Έ

5. μ»¨λ²€μ…˜

일뢀 μ»¨λ²€μ…˜/방식듀은 κ°œμ„ ν•  μ˜ˆμ •μ΄κ³ , 의견 ν™˜μ˜ν•©λ‹ˆλ‹€ πŸ₯Ί

5-1. Presenter Container ꡬ쑰

  • νŽ˜μ΄μ§€μ— λ“€μ–΄κ°€λŠ” μ΅œμƒμœ„ μ»΄ν¬λ„ŒνŠΈλ₯Ό Containers 에 λ§Œλ“­λ‹ˆλ‹€.
  β”œβ”€β”€ container
  β”‚   β”œβ”€β”€ Bakeries
  β”‚   β”œβ”€β”€ Login
  β”‚       β”œβ”€β”€ LoginContainer.tsx # {νŽ˜μ΄μ§€λͺ…μΉ­}Container
  β”‚       └── index.ts # export components
  β”‚   └── ...
  • container μ•ˆμ— λ“€μ–΄κ°€λŠ” μ»΄ν¬λ„ŒνŠΈλ“€μ€ components μ•ˆμ—μ„œ λ§Œλ“€κ³ , λΆˆλŸ¬μ˜΅λ‹ˆλ‹€.
  • μ»΄ν¬λ„ŒνŠΈμ— λ“€μ–΄κ°€λŠ” λͺ¨λ“  λ‘œμ§μ€ container에 μž‘μ„±ν•΄μ€λ‹ˆλ‹€.
// containers/LoginContainer.tsx 

export const LoginContainer = () => {
    const navigate = useNavigate();
    const {
        login: { mutate: login, error },
    } = useLogin();

    const { activate: isRemembered, onActive: onActiveRemember, onInactive: onInactiveRemeber, onToggleActive: onToggleRemember } = useToggle();
    const { form, onChangeForm, onSetForm } = useForm<LoginForm>(initialForm);

    useEffect(() => {
 
    // ...
    
return (
    <Container>
        <Wrapper>
            <Logo />
            <LoginForm form={form} onChangeForm={onChangeForm} isRemembered={isRemembered} onToggleRemember={onToggleRemember} />
            <Button type={'orange'} text={'둜그인'} onClickBtn={() => onSubmit()} />
        </Wrapper>
    </Container>
);

5-2. Module Export

  • λŒ€λΆ€λΆ„μ˜ ν΄λ”κ΅¬μ‘°λŠ” μ•„λž˜μ™€ 같이 폴더가 λ‚˜λ‰˜μ–΄μ Έμžˆκ³ , ν΄λ”λ§ˆλ‹€ index.tsκ°€ μžˆμŠ΅λ‹ˆλ‹€.
  β”œβ”€β”€ Shared
  β”‚   β”œβ”€β”€ Button
  β”‚       β”œβ”€β”€ Button.tsx 
  β”‚       └── index.ts
  β”‚   β”œβ”€β”€ Loading
  β”‚       β”œβ”€β”€ Loading.tsx
  β”‚       β”œβ”€β”€ TableLoading.tsx 
  β”‚       └── index.ts
  β”‚   β”œβ”€β”€ index.ts
  β”‚   └── ...
  • μ΄λŠ” λͺ¨λ“ˆμ„ importν•  λ•Œ import문의 쀄 수λ₯Ό 쀄이기 μœ„ν•¨μž…λ‹ˆλ‹€.
import { Button, Input, Preview, SelectBox, SelectOption } from '@/components/Shared';
  • ν•˜μ§€μ•Šμ„ 경우
import { Button } from '@/components/Shared/Button';
import { Input } from '@/components/Shared/Input';
import { Preview } from '@/components/Shared/Preview';
import { SelectBox } from '@/components/Shared/SelectBox';
import { SelectOption } from '@/components/Shared/SelectOption';

  1. μ»΄ν¬λ„ŒνŠΈ, ν•¨μˆ˜ λ“±λ“± λŒ€λΆ€λΆ„μ˜ 경우, μ„ μ–Έ μ•žμͺ½μ— export ν•΄μ€λ‹ˆλ‹€. (Named Export)
// ❌ export default BakeriesContainer;

export const BakeriesContainer = () => {
}
  1. index.tsμ—μ„œ λͺ¨λ“ˆμ„ 내보낼 λ•ŒλŠ” * 둜 전체λ₯Ό λ‚΄λ³΄λƒ…λ‹ˆλ‹€.
 // ❌ export { SkeletonCell } from './SkeletonCell';

 export * from './SkeletonCell';
  1. νƒ€μž…μ„ export 해쀄 경우 export type을 μ‚¬μš©ν•΄μ£Όμ„Έμš”.
export type { TableHeader, TableCell, TableProps } from './types';

Named Export, Default Export 참고자료

5-3. API Context

  • μ‹€ μ„œλΉ„μŠ€μ— μ‚¬μš©λ˜λŠ” apiλŠ” Context APIλ₯Ό μ΄μš©ν•©λ‹ˆλ‹€.
  • ν…ŒμŠ€νŠΈμ— μš©μ΄ν•˜κ²Œ DI(Dependency Injection, μ˜μ‘΄μ„± μ£Όμž…)λ₯Ό μ΄μš©ν•˜μ—¬ κ΅¬ν˜„ν•©λ‹ˆλ‹€.
  • μ‚¬μš©λ˜λŠ” api, context ν΄λ”μ˜ κ΅¬μ‘°λŠ” λ‹€μŒκ³Ό κ°™μŠ΅λ‹ˆλ‹€.
  β”œβ”€β”€ api
  β”‚   β”œβ”€β”€ bakery
  β”‚       β”œβ”€β”€ bakery.ts
  β”‚       β”œβ”€β”€ bakeryClient.ts
  β”‚       β”œβ”€β”€ useBakeries..ts
  β”‚       β”œβ”€β”€ useBakery.ts
  β”‚       β”œβ”€β”€ types.ts
  β”‚       └── index.ts
  β”‚   └── ...
  β”œβ”€β”€ context
  β”‚   β”œβ”€β”€ bakery
  β”‚       β”œβ”€β”€ BakeryApiContext.tsx
  β”‚       β”œβ”€β”€ BakeryApiProvider.tsx
  β”‚       └── index.ts
  β”‚   β”œβ”€β”€ ...
  β”‚   └── ApiProvider.ts
  1. clientλ₯Ό μ£Όμž…λ°›μ„ api class와 client classλ₯Ό μ„ μ–Έν•©λ‹ˆλ‹€.
// api/bakery/kbakery.ts

export class Bakery {
  constructor(public client: BakeryApiClient) {}

  async getItem({ bakeryId }: { bakeryId: number }) {
    const item = await this.client.getItem({ bakeryId });
    return item;
  }

  async createItem({ payload }: CreateUpdateBakeryPayload) {
    await this.client.createItem({ payload });
  }
// apis/bakery/bakeryClient.ts

export class BakeryClient implements BakeryApiClient {
  async getItem({ bakeryId }: { bakeryId: number }) {
    const resp = await fetcher.get<BakeryDetailEntity>(`bakery/${bakeryId}`);
    return resp.data;
  }

  async createItem({ payload }: CreateUpdateBakeryPayload) {
    await fetcher.post('bakery', payload, {
      headers: {
        'Content-Type': 'multipart/form-data',
      },
    });
  }
  1. ν•΄λ‹Ή api의 context와 providerλ₯Ό λ§Œλ“€κ³ , ApiProvider에 μΆ”κ°€ν•΄μ€λ‹ˆλ‹€.
// context/bakery/BakeryApiContext.tsx

export const BakeryApiContext = createContext<{ bakery: Bakery | null }>({ bakery: null });

export const useBakeryApi = () => {
  return useContext(BakeryApiContext);
};
// context/bakery/BakeryApiProvider.tsx

const client = new BakeryClient();
const bakery = new Bakery(client);

export const BakeryApiProvider = ({ children }: { children: ReactNode }) => {
  return <BakeryApiContext.Provider value={{ bakery }}>{children}</BakeryApiContext.Provider>;
};
// context/ApiProvider.tsx

export const ApiProvider = ({ children }: { children: ReactNode }) => {
  return (
    <BakeryApiProvider>
      <BakeryReportApiProvider>{children}</BakeryReportApiProvider>
    </BakeryApiProvider>
  );
};
  1. React Queryλ₯Ό μ΄μš©ν•΄μ„œ Query hook을 μ„ μ–Έν•  λ•Œ λΆˆλŸ¬μ™€μ„œ μ‚¬μš©ν•©λ‹ˆλ‹€.
// apis/bakery/useBakery.ts

export const useBakery = ({ bakeryId }: { bakeryId: number }) => {
  const { bakery } = useBakeryApi();
  const queryClient = useQueryClient();

  if (!bakery) {
    throw new Error('bakeryApiλ₯Ό ν™•μΈν•΄μ£Όμ„Έμš”.');
  }

  const bakeryQuery = useQuery(['bakery', { bakeryId }], () => bakery.getItem({ bakeryId }), {
    enabled: !isNaN(bakeryId),
  });

  const addBakery = useMutation(bakery.createItem, {
    onSuccess: () => queryClient.invalidateQueries('getBakeries'),
  });
  1. ν™”λ©΄μ—μ„œ apiλ₯Ό μ΄μš©ν•  λ•ŒλŠ” Query hook을 μ΄μš©ν•©λ‹ˆλ‹€.
// containers/bakeryDetail/BakeryDetailContainers.tsx

const {
    bakeryQuery: { data: bakery },
    addBakery,
    editBakery,
} = useBakery({ bakeryId: Number(bakeryId) });

// ...

const onCreateForm = (payload: FormData) => {
    addBakery.mutate( 
        // ... 

6. ν…ŒμŠ€νŠΈ

6-1. ν…ŒμŠ€νŠΈ μ‹€ν–‰ 방법

{
  "test": "jest --watchAll",
  "test:coverage": "jest --coverage --watchAll"
}

6-2. μž‘μ„±μ€‘..

About

bread-map-admin ver 2 🐢

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • TypeScript 95.8%
  • JavaScript 1.8%
  • CSS 1.5%
  • Other 0.9%