그립 기획서 작성 : 피그마로 개인프로젝트 그립 무비 앱 기획서를 작성하였다. but 아직 미완성...
그립 무비 앱 코드 리뷰 : 로켓런치 장수영님이 무비 앱 코드 리뷰를 진행해주셨다.
타입스크립트 연습 : 타입스크립트 적용하는 것 연습해보기
// 1
onClick?: (e: MouseEvent<HTMLButtonElement>) => void
setChecked?: (e: ChangeEvent<HTMLInputElement>) => void
onKeyDown?: (e: KeyboardEvent<HTMLInputElement>) => void
postSubmit: (e: FormEvent<HTMLFormElement>) => void
// 2
interface ButtonProps extends Omit<ButtonHTMLAttributes<unknown>, 'translate'> {
children: string | JSX.Element | JSX.Element[]
className?: string
disabled?: boolean
isSubmit?: boolean
loading?: boolean
name?: string
onClick?: (e: MouseEvent<HTMLButtonElement>) => void
btnRef?: RefObject<HTMLButtonElement>
size: 'unset' | 'big' | 'medium' | 'small' | 'smallest'
shape: 'unset' | 'reward' | 'rounded' | 'primary' | 'order' | 'ghost' | 'ghostLight' | 'line'
translate?: boolean
url?: string
inline?: boolean
danger?: boolean // ghost 빨간 버전
}
interface ButtonProps {
text: string
type:
| 'textWithArrow'
| 'textWithCopied'
| 'textWithIcon'
| 'copyButtonOnly'
| 'iconOnly'
| 'textOnly'
| 'textToCopied'
handleCopy?: () => void
}
interface Props {
className?: string
children: ReactNode
asFooter?: boolean
}
// 3
interface Props {
transactionUrl: string
currency: string
txId: string
transferState: ETransferState
arrow?: boolean
inline?: boolean
shape: 'line' | 'rounded'
}
export const ButtonTXID = ({
transactionUrl,
currency,
txId,
transferState,
arrow,
inline,
shape,
}: Props): JSX.Element | null => {
if (!currency || !txId) return null
if (!isTxidAvailable(transferState)) return null
return (
<Button url={transactionUrl} className={styles.txidButton} shape={shape} size='unset' inline={inline}>
<span>TX ID</span>
{arrow ? <ArrowIcon /> : <></>}
</Button>
)
}
// 4
import styles, { cx } from 'styles'
import { useI18n } from 'hooks'
import Lottie from 'utils/lottie'
import LoadingAni from '@/assets/animations/loading.json'
import LoadingAniDark from '@/assets/animations/loading-dark.json'
interface Props {
size: 'tiny' | 'small' | 'big' | 'bigger' | 'large' | 'huge'
withLabel?: boolean
className?: string
forceDark?: boolean
forceBright?: boolean
full?: boolean
}
export const LoadingSpinner = (props: Props): JSX.Element => {
const { size, withLabel, className, forceDark, forceBright, full } = props
const t = useI18n()
const wrapperClassName = cx(styles.loadingSpinner, className, styles[size], {
[styles.forceDark]: forceDark,
[styles.forceBright]: forceBright,
[styles.full]: full,
})
return (
<div className={wrapperClassName}>
<Lottie animationData={LoadingAni} className={styles.brightSpinner} />
<Lottie animationData={LoadingAniDark} className={styles.darkSpinner} />
{withLabel && <p>{t('front:global.loading')}</p>}
</div>
)
}
// 5
function getActiveWidth(
percentage: number,
isDesktop: boolean,
mobileMaxWidth: number,
atTrade?: boolean,
atWallet?: boolean
): number {
if (atTrade) {
const maxWidth = isDesktop ? 295 : mobileMaxWidth
const dirtyWidth = maxWidth * percentage
const cleanWidth = dirtyWidth - (dirtyWidth % 5) // 5의 배수로 고정
return Math.max(cleanWidth, 5)
}
if (atWallet) {
const maxWidth = isDesktop ? 480 : mobileMaxWidth
const dirtyWidth = maxWidth * percentage
const cleanWidth = dirtyWidth - (dirtyWidth % 8) // 8의 배수로 고정
return Math.max(cleanWidth, 8)
}
return 0
}
// 6
import { Dispatch, MouseEvent, SetStateAction } from 'react'
import styles, { cx } from 'styles'
import { useI18n } from 'hooks'
import { LoadingSpinner } from '@/components/_common/LoadingSpinner'
export interface SwitchItem {
name: string
label?: string
icon?: JSX.Element
disabled?: boolean
}
interface Props<T> {
items: SwitchItem[]
mode: T
setMode: (newMode: T) => void | Dispatch<SetStateAction<T>>
className?: string
type: 'dTag' | 'default'
color: 'common' | 'trade' | 'setting'
isLoading?: boolean
}
export const CommonSwitch = <T extends string>({
items,
mode,
setMode,
className,
type,
color,
isLoading,
}: Props<T>): JSX.Element => {
const t = useI18n()
const handleMode = (e: MouseEvent<HTMLButtonElement>): void => {
const { name } = e.currentTarget
setMode(name as T)
}
const wrapperClassName = cx(styles.commonSwitch, styles[type], styles[color], className, {
[styles.right]: mode === items[1].name,
})
if (isLoading) {
return (
<div className={wrapperClassName}>
<LoadingSpinner size='small' forceDark />
</div>
)
}
return (
<div className={wrapperClassName}>
{items.map((item) => {
return (
<button
key={commonSwitch-${item.name}}
type='button'
onClick={handleMode}
name={item.name}
disabled={item.disabled}
className={cx({ [styles.active]: mode === item.name })}
tabIndex={-1}
>
{item.icon || t(item.label || '')}
</button>
)
})}
<div className={styles.aniBg} />
</div>
)
}
// 7
import { MouseEvent } from 'react'
import DatePicker from 'react-datepicker'
import styles from 'styles'
import { SetterOrUpdater } from 'stateHooks'
import { useClickAway, useGA, useI18n, useRef, useState } from 'hooks'
import { TDateFilter } from 'types/histories.d'
import { Button } from '../_common/Button'
import DatePickerHeader from './DatePickerHeader'
import { CommonButtons } from '@/components/_common/Buttons'
interface Props {
date: TDateFilter
setDate: SetterOrUpdater<TDateFilter> | ((value: Date | null) => void)
handleClose: () => void
gaAction: string
}
const CustomDatePicker = ({ date, setDate, handleClose: handleClose2, gaAction }: Props): JSX.Element => {
const datePickerRef = useRef<HTMLDivElement>(null)
const { gaEvent } = useGA()
const t = useI18n()
const [selected, setSelected] = useState(date)
useClickAway(datePickerRef, handleClose2)
const renderDayContents = (day: number): JSX.Element => {
return (
<div>
<span className={styles.dayContent}>{day}</span>
</div>
)
}
const handleChange = (d: Date): void => setSelected(d)
const handleConfirm = (): void => {
setDate(selected)
handleClose2()
gaEvent({ action: `${gaAction}_calendar_search` })
}
const handleClose = (e: MouseEvent<HTMLButtonElement>) => {
return e
}
// 8
return (
<div className={styles.datePicker} ref={datePickerRef} translate='no'>
<DatePicker
selected={selected}
onChange={handleChange}
inline
renderDayContents={renderDayContents}
maxDate={new Date()}
showMonthDropdown
useShortMonthInDropdown
renderCustomHeader={({ date: d, changeYear, changeMonth, decreaseMonth, increaseMonth }): JSX.Element => (
<DatePickerHeader
date={d}
changeYear={changeYear}
changeMonth={changeMonth}
decreaseMonth={decreaseMonth}
increaseMonth={increaseMonth}
/>
)}
/>
<CommonButtons asFooter>
<Button shape='ghost' size='big' onClick={handleClose} translate>
{t('front:global.cancel')}
</Button>
<Button shape='primary' size='big' onClick={handleConfirm} translate>
{t('front:global.go')}
</Button>
</CommonButtons>
</div>
)
}
export default CustomDatePicker
// 9
import { ChangeEvent, Dispatch, MouseEvent, SetStateAction, useMemo } from 'react'
import styles, { cx } from 'styles'
import { useState } from 'hooks'
import { PageHandleIcon, TooltipIcon } from 'svgs'
interface SliderProps {
currentPage: number
totalPage: number
tooltips?: string[]
setPage: Dispatch<SetStateAction<number>> | ((value: number) => void)
wrapperWidth: number
scrollToTop: () => void
}
const Slider = ({
currentPage,
totalPage,
tooltips,
setPage,
wrapperWidth,
scrollToTop,
}: SliderProps): JSX.Element | null => {
const [hover, setHover] = useState<{
date?: string
x?: number
}>({})
const sliderX = useMemo(() => currentPage - 1, [currentPage])
const handleOnChange = (event: ChangeEvent<HTMLInputElement>): void => {
const { value } = event.currentTarget
setPage(Number(value) + 1)
}
const handleMouseOut = (): void => setHover({})
const handleHover = (e: MouseEvent): void => {
if (!tooltips) return
const { x } = e.currentTarget.getBoundingClientRect()
const hoverWidth = wrapperWidth / totalPage
const hoveredPageNumber = parseInt(`${(e.clientX - x) / hoverWidth}`, 10)
const date = tooltips[hoveredPageNumber]
if (!date) return
setHover({
date,
x: e.clientX - x,
})
}
// 10
return (
<div className={styles.sliderWrapper} onMouseMove={handleHover} onMouseOut={handleMouseOut} onBlur={handleMouseOut}>
<input
type='range'
step={0}
min={0}
max={totalPage - 1}
value={sliderX}
onChange={handleOnChange}
onMouseUp={scrollToTop}
/>
<div
className={cx(styles.fakeHandle, { [styles.hover]: hover.date })}
style={{
left: `${(wrapperWidth / totalPage) * sliderX}px`,
width: `${wrapperWidth / totalPage}px``,
}}
>
<PageHandleIcon />
</div>
{hover.date && (
<div className={styles.hoverDate} style={{ left: hover.x }}>
<p>{hover.date}</p>
<TooltipIcon role='tooltip' />
</div>
)}
<div className={styles.fakeTrack} />
</div>
)
}
export default Slider
// 11
export interface WalletDataCore {
flatFee: string
rateFee: string
minAmount: string
maxAmount: string
validFrom: number
}
export interface FeeListItem extends WalletDataCore {
blockchainName: string
currency: string
}
export interface FeeListData {
withdrawalFees: FeeListItem[]
}
export interface WithdrawBalanceData extends WalletDataCore {
withdrawableAmount24h: string
}
export interface AddressData {
address: string
addressParams?: {
destinationTag: string
}
}
export interface WithdrawalParams {
blockchainName?: string
currency: string
address: string
addressParams?: {
destinationTag: string
}
netAmount: string
fee: string
token: string
}
export interface WithdrawalSucessData {
id: string
}
export interface WithdrawalErrorData {
error: string
errorDetails: {
reason: string
expiredAt: number
}
}
// 12
/* eslint-disable camelcase */
export interface ZendeskArticle {
id: number
html_url: string
created_at: string
section_id: number
title: string
body: string
}
export interface ZendeskArticles {
articles: ZendeskArticle[]
count: number
next_page: boolean
page: number
page_count: number
per_page: number
previous_page: boolean
sort_by: string
sort_order: string
}
type AType = 'aa' | 'bbb' | 'ccc'
interface AInter {
children?: ReactNode:
}
interface BInter extends AInter {
...
}
type BType = AType & | {}
*
로 설정하면 된다.export const endDateState = atom<TDateFilter>({
key: "#exportExcel_endDateState",
default: null,
});
export const errorState = atom({
key: "#exportExcel_errorState",
default: "",
});
export const disabledSelector = selector({
key: "#exportExcel_disabledSelector",
get: ({ get }) => {
const error = get(errorState);
const startDate = get(startDateState);
const endDate = get(endDateState);
if (!startDate || !endDate || error) return true;
return false;
},
});
오늘은 피그마를 통해 그립컴퍼니의 개인프로젝트 과제에 기획서를 작성하였다. 오랜만에 기획서를 보니 퍼블리싱하던 시절이 생각났다.
디자인이 자유였고 기능에 대한 정리가 필요했기 때문에 피그마를 통해 기획서를 새로 작성하였다. 오랜만에 기획서를 작성하다보니 머리가 돌아가질 않았던 것 같다.
내일은 꼭 완성하고 프로젝트 초기 세팅까지 마무리해야겠다. 또한 오늘 강의 시간에는 게스트(?)한 분이 오셨다.
로켓런치 이준혁님의 동료이신 5년차 개발자 장수영님이 오셨다. 장수영님께 코드 리뷰를 받는 시간을 가졌는데 짧은 시간이었지만 정말 알찬 시간이었다.
장수영님이 설명해주신 것들 중에 이해가 가지 않는 부분들도 존재했지만 분명 짧은 시간에 많은 것을 배웠으며 공부할 수 있는 키워드들을 많이 알 수 있는 시간이었다.
또한 장수영님을 보고 느낀점은 내가 5년차 개발자가 된다면 저만큼 성장해야겠다는 목표를 세우게 된 것 같다.
장수영님의 코드 리뷰가 끝난 뒤 먼저 퇴장하셨는데 그 후 이준혁님이 말씀해주시기를 장수영님은 퇴근 후에도 계속해서 개발을 공부한다고 하셨다.
또 코드 작성 시 문제에 부딪히게 되면 우회하여 피해가는 경우가 있는데 그런 것이 아닌 정확히 알고 넘어가는 스타일이라고 하셨다.
참 배울 분이 많으신 선배 개발자셨다. 나도 5년차에는 물경력 쌓인 아무것도 모르는 가성비 나쁜 개발자가 아닌 5년차에 맞는 고급 개발자가 되고 싶다.
오늘 강의 시간은 정말 알찬 시간이었고 강의 시간마다 항상 강의 시간을 초과하는 열정을 보여주시며 또 참가자들의 발전을 위해 게스트까지 초대해주시며 정말 감사했다.