State Management 5.4_ Recap
92212 ワード
State Management
appとchartでatomの値を得ることができます
見出し
atom値を変更することはできます.
パラメータはatomを受信し、atomの関数を変更します.
動作はreactのsetStateと同じです.
彼に以前の値段を変えさせた.
これはクールな方法です.私の構成部品がatom値を観察し始めると、atomが変化すると、他の構成部品も再レンダリングされます.atomを作成するのは、一意のキーとポーリング値がある限り簡単です.このdefault値は関数で置き換えることができます.これが全てです.
App.tsx import { createGlobalStyle } from 'styled-components';
import Router from './Router';
import { ReactQueryDevtools } from 'react-query/devtools';
import { ThemeProvider } from 'styled-components';
import { darkTheme, lightTheme } from './theme';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDarkAtom } from './atoms';
const GlobalStyle = createGlobalStyle`
@import url('https://fonts.googleapis.com/css2?family=Archivo+Narrow:wght@500&family=Bebas+Neue&family=Black+Han+Sans&family=Do+Hyeon&family=Source+Sans+Pro:wght@300;400&family=Ubuntu+Mono:ital@1&display=swap');
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, menu, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
main, menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, main, menu, nav, section {
display: block;
}
/* HTML5 hidden-attribute fix for newer browsers */
*[hidden] {
display: none;
}
body {
line-height: 1;
}
menu, ol, ul, li {
list-style: none;
}
button{
background-color: transparent;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
*{
box-sizing: border-box;
}
body{
font-family: 'Source Sans Pro', sans-serif;
background-color: ${(props) => props.theme.bgColor};
color : ${(props) => props.theme.textColor};
}
a{
text-decoration: none;
color:inherit;
}
`;
export default function App() {
const isDark = useRecoilValue(isDarkAtom);
return (
<>
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<GlobalStyle />
<Router />
<ReactQueryDevtools initialIsOpen={true} />
</ThemeProvider>
</>
);
}
index.tsx import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { QueryClient, QueryClientProvider } from 'react-query';
import { RecoilRoot } from 'recoil';
const queryClient = new QueryClient();
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</RecoilRoot>
</React.StrictMode>,
document.getElementById('root')
);
Coin.tsx import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { useQuery } from 'react-query';
import { fetchCoins } from './api';
import { Helmet } from 'react-helmet';
import Header from './Header';
const Container = styled.div`
padding: 0px 20px;
max-width: 480px;
margin: 0 auto;
`;
const CoinsList = styled.ul`
padding-top: 13vh;
font-weight: bold;
font-size: 20px;
`;
const Coin = styled.li`
background-color: white;
color: ${(props) => props.theme.bgColor};
margin-bottom: 10px;
padding: 20px;
border-radius: 15px;
a {
padding: 5px; // 좀 더 넓은 범위에서 transition 효과 적용 가능
transition: color 0.2s ease-in;
}
&:hover {
a {
color: ${(props) => props.theme.accentColor};
}
// 아래에서는 a가 아닌 Link라는 이름으로 사용했지만
// css에서는 anchor 를 선택해야 했다. 이건 모든 react router link들이
// 결국에는 anchor로 바뀔거기도 하고,
// react router dom이 우리 대신 설정을 도와줄 특별한 event listener들이 있기도 하다
}
`;
const Loader = styled.span`
text-align: center;
display: block;
`;
const Img = styled.img`
width: 25px;
height: 25px;
margin-right: 10px;
`;
interface ICoin {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
}
function Coins() {
const { isLoading, data } = useQuery<ICoin[]>('allCoins', fetchCoins);
console.log(isLoading, data);
return (
<>
<Header />
<Container>
<Helmet>
<title>코인</title>
</Helmet>
{isLoading ? (
<Loader>"Loading..."</Loader>
) : (
//loading 이 참이면 Loading... 출력, 거짓이면 CoinsList 보여줌
<CoinsList>
{data?.slice(0, 100).map((coin) => (
<Coin key={coin.id}>
<Img
src={`https://raw.githubusercontent.com/ErikThiart/cryptocurrency-icons/master/16/${coin.name
.toLowerCase()
.split(' ')
.join('-')}.png`}
/>
<Link
to={{
pathname: `/${coin.id}`,
state: { name: coin.name },
//Link를 이용해 string 이외에 더 많은 데이터를 보낼 수 있다
}}
>
{coin.id}
</Link>
</Coin>
))}
</CoinsList>
)}
</Container>
</>
);
}
export default Coins;
Coins.tsx import { useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { Switch, Route, useLocation, useParams, useRouteMatch } from 'react-router';
import { Link } from 'react-router-dom';
import Header from './Header';
import styled from 'styled-components';
import Chart from './Chart';
import Price from './Price';
import { fetchCoinInfo, fetchCoinTickers } from './api';
import { Helmet } from 'react-helmet';
import { useHistory } from 'react-router-dom';
function Coin() {
const { coinId } = useParams<RouteParams>();
const { state } = useLocation<RouteState>();
const priceMatch = useRouteMatch('/:coinId/price');
const chartMatch = useRouteMatch('/:coinId/chart');
const { isLoading: infoLoading, data: infoData } = useQuery<InfoData>(['info', coinId], () =>
fetchCoinInfo(coinId)
);
const { isLoading: tickersLoading, data: tickerData } = useQuery<PriceData>(
['tickers', coinId],
() => fetchCoinTickers(coinId),
{
refetchInterval: 5000,
}
);
const loading = infoLoading || tickersLoading;
return (
<>
<Header />
<Container>
<Helmet>
<title>{state?.name ? state.name : loading ? 'Loading...' : infoData?.name}</title>
</Helmet>
<Title>{state?.name ? state.name : loading ? 'Loading...' : infoData?.name}</Title>
{loading ? (
<Loader>Loading...</Loader>
) : (
<>
<Overview>
<OverviewItem>
<span>Rank:</span>
<span>{infoData?.rank}</span>
</OverviewItem>
<OverviewItem>
<span>Symbol:</span>
<span>${infoData?.symbol}</span>
</OverviewItem>
<OverviewItem>
<span>Price:</span>
<span>{tickerData?.quotes.USD.price.toFixed(3)}</span>
</OverviewItem>
</Overview>
<Description>{infoData?.description}</Description>
<Overview>
<OverviewItem>
<span>Total Suply:</span>
<span>{tickerData?.total_supply}</span>
</OverviewItem>
<OverviewItem>
<span>Max Supply:</span>
<span>{tickerData?.max_supply}</span>
</OverviewItem>
</Overview>
<Tabs>
<Tab isActive={chartMatch !== null}>
<Link to={`/${coinId}/chart`}>Chart</Link>
</Tab>
<Tab isActive={priceMatch !== null}>
<Link to={`/${coinId}/price`}>Price</Link>
</Tab>
</Tabs>
{/* 다양한 URL 로 Switch 하기 */}
<Switch>
<Route path={`/${coinId}/price`}>
<Price coinId={coinId} />
</Route>
<Route path={`/${coinId}/chart`}>
<Chart coinId={coinId} isDark={false} />
</Route>
</Switch>
</>
)}
</Container>
</>
);
}
interface RouteState {
name: string;
}
interface RouteParams {
coinId: string;
}
interface InfoData {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
description: string;
message: string;
open_source: boolean;
started_at: string;
development_status: string;
hardware_wallet: boolean;
proof_type: string;
org_structure: string;
hash_algorithm: string;
first_data_at: string;
last_data_at: string;
}
interface PriceData {
id: string;
name: string;
symbol: string;
rank: number;
circulating_supply: number;
total_supply: number;
max_supply: number;
beta_value: number;
first_data_at: string;
last_updated: string;
quotes: {
USD: {
ath_date: string;
ath_price: number;
market_cap: number;
market_cap_change_24h: number;
percent_change_1h: number;
percent_change_1y: number;
percent_change_6h: number;
percent_change_7d: number;
percent_change_12h: number;
percent_change_15m: number;
percent_change_24h: number;
percent_change_30d: number;
percent_change_30m: number;
percent_from_price_ath: number;
price: number;
volume_24h: number;
volume_24h_change_24h: number;
};
};
}
const Container = styled.div`
padding: 0px 20px;
padding-top: 13vh;
max-width: 480px;
margin: 0 auto;
`;
const Title = styled.h1`
text-align: center;
padding-bottom: 5%;
font-size: 50px;
color: ${(props) => props.theme.accentColor};
`;
const Loader = styled.span`
display: block;
text-align: center;
`;
const Overview = styled.div`
display: flex;
justify-content: space-between;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px 20px;
border-radius: 10px;
`;
const OverviewItem = styled.div`
display: flex;
flex-direction: column;
align-items: center;
span:first-child {
font-size: 10px;
font-weight: 400;
text-transform: uppercase;
margin-bottom: 5px;
}
`;
const Description = styled.p`
margin: 20px 0px;
`;
const Tabs = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr);
margin: 25px 0px;
gap: 10px;
`;
const Tab = styled.span<{ isActive: boolean }>`
//props 받기
text-align: center;
text-transform: uppercase;
font-size: 12px;
font-weight: 400;
background-color: rgba(0, 0, 0, 0.5);
padding: 7px 0px;
border-radius: 10px;
color: ${(props) => (props.isActive ? props.theme.accentColor : props.theme.textColor)};
a {
display: block;
}
`;
export default Coin;
Chart.tsx import { useQuery } from 'react-query';
import { fetchCoinHistory } from './api';
import ApexChart from 'react-apexcharts';
import { useRecoilValue } from 'recoil';
import { isDarkAtom } from '../atoms';
interface IHistorycal {
time_open: string;
time_close: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
market_cap: number;
}
interface ChartProps {
coinId: string;
isDark: boolean;
}
function Chart({ coinId, isDark }: ChartProps) {
const { isLoading, data } = useQuery<IHistorycal[]>(
['ohlcv', coinId],
() => fetchCoinHistory(coinId),
{
refetchInterval: 10000,
}
);
//14개를 받아와야 하므로 배열로 전달.
return (
<div>
{isLoading ? (
'Loading chart...'
) : (
<ApexChart
type="line"
series={[
{
name: 'price',
data: data?.map((price) => price.close) as number[],
},
]}
options={{
theme: {
mode: isDark ? 'dark' : 'light',
},
chart: {
width: 500,
height: 300,
toolbar: { show: false },
background: 'transparents',
},
grid: { show: false },
stroke: { curve: 'smooth', width: 3 },
yaxis: { show: false },
xaxis: {
type: 'datetime',
categories: data?.map((price) => price.time_close),
},
fill: {
type: 'gradient',
gradient: { gradientToColors: ['#0be881'], stops: [0, 100] },
},
colors: ['blue'],
tooltip: { y: { formatter: (value) => `$ ${value.toFixed(3)}` } },
}}
/>
)}
</div>
);
}
export default Chart;
Header.tsx import styled from 'styled-components';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import BrightnessMediumIcon from '@mui/icons-material/BrightnessMedium';
import { useHistory } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { isDarkAtom } from '../atoms';
export default function Header() {
let history = useHistory();
const goBack = () => {
history.goBack();
};
const setterDarkAtom = useSetRecoilState(isDarkAtom);
const toggleDarkAtom = () => setterDarkAtom((prev) => !prev);
return (
<>
<Wrapper>
<Wrapper2>
<button onClick={goBack}>
<ArrowBackIosNewIcon sx={{ fontSize: 30, color: 'white' }} />
</button>
<span>Coin Tracker</span>
<button onClick={toggleDarkAtom}>
<BrightnessMediumIcon sx={{ fontSize: 30, color: 'white' }} />
</button>
</Wrapper2>
</Wrapper>
</>
);
}
const Wrapper = styled.div`
background-color: #1f2b38;
position: fixed;
width: 100vw;
height: 10vh;
`;
const Wrapper2 = styled.div`
background-color: #1f2b38;
max-width: 500px;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 auto;
span {
font-weight: bold;
color: ${(props) => props.theme.accentColor};
}
`;
atom.ts import { atom } from 'recoil';
export const isDarkAtom = atom({
key: 'isDark',
default: false,
});
Reference
この問題について(State Management 5.4_ Recap), 我々は、より多くの情報をここで見つけました
https://velog.io/@angel_eugnen/State-Management-5.4-Recap
テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol
import { createGlobalStyle } from 'styled-components';
import Router from './Router';
import { ReactQueryDevtools } from 'react-query/devtools';
import { ThemeProvider } from 'styled-components';
import { darkTheme, lightTheme } from './theme';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDarkAtom } from './atoms';
const GlobalStyle = createGlobalStyle`
@import url('https://fonts.googleapis.com/css2?family=Archivo+Narrow:wght@500&family=Bebas+Neue&family=Black+Han+Sans&family=Do+Hyeon&family=Source+Sans+Pro:wght@300;400&family=Ubuntu+Mono:ital@1&display=swap');
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, menu, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
main, menu, nav, output, ruby, section, summary,
time, mark, audio, video {
margin: 0;
padding: 0;
border: 0;
font-size: 100%;
font: inherit;
vertical-align: baseline;
}
/* HTML5 display-role reset for older browsers */
article, aside, details, figcaption, figure,
footer, header, hgroup, main, menu, nav, section {
display: block;
}
/* HTML5 hidden-attribute fix for newer browsers */
*[hidden] {
display: none;
}
body {
line-height: 1;
}
menu, ol, ul, li {
list-style: none;
}
button{
background-color: transparent;
}
blockquote, q {
quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content: '';
content: none;
}
table {
border-collapse: collapse;
border-spacing: 0;
}
*{
box-sizing: border-box;
}
body{
font-family: 'Source Sans Pro', sans-serif;
background-color: ${(props) => props.theme.bgColor};
color : ${(props) => props.theme.textColor};
}
a{
text-decoration: none;
color:inherit;
}
`;
export default function App() {
const isDark = useRecoilValue(isDarkAtom);
return (
<>
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<GlobalStyle />
<Router />
<ReactQueryDevtools initialIsOpen={true} />
</ThemeProvider>
</>
);
}
index.tsx import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { QueryClient, QueryClientProvider } from 'react-query';
import { RecoilRoot } from 'recoil';
const queryClient = new QueryClient();
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</RecoilRoot>
</React.StrictMode>,
document.getElementById('root')
);
Coin.tsx import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { useQuery } from 'react-query';
import { fetchCoins } from './api';
import { Helmet } from 'react-helmet';
import Header from './Header';
const Container = styled.div`
padding: 0px 20px;
max-width: 480px;
margin: 0 auto;
`;
const CoinsList = styled.ul`
padding-top: 13vh;
font-weight: bold;
font-size: 20px;
`;
const Coin = styled.li`
background-color: white;
color: ${(props) => props.theme.bgColor};
margin-bottom: 10px;
padding: 20px;
border-radius: 15px;
a {
padding: 5px; // 좀 더 넓은 범위에서 transition 효과 적용 가능
transition: color 0.2s ease-in;
}
&:hover {
a {
color: ${(props) => props.theme.accentColor};
}
// 아래에서는 a가 아닌 Link라는 이름으로 사용했지만
// css에서는 anchor 를 선택해야 했다. 이건 모든 react router link들이
// 결국에는 anchor로 바뀔거기도 하고,
// react router dom이 우리 대신 설정을 도와줄 특별한 event listener들이 있기도 하다
}
`;
const Loader = styled.span`
text-align: center;
display: block;
`;
const Img = styled.img`
width: 25px;
height: 25px;
margin-right: 10px;
`;
interface ICoin {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
}
function Coins() {
const { isLoading, data } = useQuery<ICoin[]>('allCoins', fetchCoins);
console.log(isLoading, data);
return (
<>
<Header />
<Container>
<Helmet>
<title>코인</title>
</Helmet>
{isLoading ? (
<Loader>"Loading..."</Loader>
) : (
//loading 이 참이면 Loading... 출력, 거짓이면 CoinsList 보여줌
<CoinsList>
{data?.slice(0, 100).map((coin) => (
<Coin key={coin.id}>
<Img
src={`https://raw.githubusercontent.com/ErikThiart/cryptocurrency-icons/master/16/${coin.name
.toLowerCase()
.split(' ')
.join('-')}.png`}
/>
<Link
to={{
pathname: `/${coin.id}`,
state: { name: coin.name },
//Link를 이용해 string 이외에 더 많은 데이터를 보낼 수 있다
}}
>
{coin.id}
</Link>
</Coin>
))}
</CoinsList>
)}
</Container>
</>
);
}
export default Coins;
Coins.tsx import { useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { Switch, Route, useLocation, useParams, useRouteMatch } from 'react-router';
import { Link } from 'react-router-dom';
import Header from './Header';
import styled from 'styled-components';
import Chart from './Chart';
import Price from './Price';
import { fetchCoinInfo, fetchCoinTickers } from './api';
import { Helmet } from 'react-helmet';
import { useHistory } from 'react-router-dom';
function Coin() {
const { coinId } = useParams<RouteParams>();
const { state } = useLocation<RouteState>();
const priceMatch = useRouteMatch('/:coinId/price');
const chartMatch = useRouteMatch('/:coinId/chart');
const { isLoading: infoLoading, data: infoData } = useQuery<InfoData>(['info', coinId], () =>
fetchCoinInfo(coinId)
);
const { isLoading: tickersLoading, data: tickerData } = useQuery<PriceData>(
['tickers', coinId],
() => fetchCoinTickers(coinId),
{
refetchInterval: 5000,
}
);
const loading = infoLoading || tickersLoading;
return (
<>
<Header />
<Container>
<Helmet>
<title>{state?.name ? state.name : loading ? 'Loading...' : infoData?.name}</title>
</Helmet>
<Title>{state?.name ? state.name : loading ? 'Loading...' : infoData?.name}</Title>
{loading ? (
<Loader>Loading...</Loader>
) : (
<>
<Overview>
<OverviewItem>
<span>Rank:</span>
<span>{infoData?.rank}</span>
</OverviewItem>
<OverviewItem>
<span>Symbol:</span>
<span>${infoData?.symbol}</span>
</OverviewItem>
<OverviewItem>
<span>Price:</span>
<span>{tickerData?.quotes.USD.price.toFixed(3)}</span>
</OverviewItem>
</Overview>
<Description>{infoData?.description}</Description>
<Overview>
<OverviewItem>
<span>Total Suply:</span>
<span>{tickerData?.total_supply}</span>
</OverviewItem>
<OverviewItem>
<span>Max Supply:</span>
<span>{tickerData?.max_supply}</span>
</OverviewItem>
</Overview>
<Tabs>
<Tab isActive={chartMatch !== null}>
<Link to={`/${coinId}/chart`}>Chart</Link>
</Tab>
<Tab isActive={priceMatch !== null}>
<Link to={`/${coinId}/price`}>Price</Link>
</Tab>
</Tabs>
{/* 다양한 URL 로 Switch 하기 */}
<Switch>
<Route path={`/${coinId}/price`}>
<Price coinId={coinId} />
</Route>
<Route path={`/${coinId}/chart`}>
<Chart coinId={coinId} isDark={false} />
</Route>
</Switch>
</>
)}
</Container>
</>
);
}
interface RouteState {
name: string;
}
interface RouteParams {
coinId: string;
}
interface InfoData {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
description: string;
message: string;
open_source: boolean;
started_at: string;
development_status: string;
hardware_wallet: boolean;
proof_type: string;
org_structure: string;
hash_algorithm: string;
first_data_at: string;
last_data_at: string;
}
interface PriceData {
id: string;
name: string;
symbol: string;
rank: number;
circulating_supply: number;
total_supply: number;
max_supply: number;
beta_value: number;
first_data_at: string;
last_updated: string;
quotes: {
USD: {
ath_date: string;
ath_price: number;
market_cap: number;
market_cap_change_24h: number;
percent_change_1h: number;
percent_change_1y: number;
percent_change_6h: number;
percent_change_7d: number;
percent_change_12h: number;
percent_change_15m: number;
percent_change_24h: number;
percent_change_30d: number;
percent_change_30m: number;
percent_from_price_ath: number;
price: number;
volume_24h: number;
volume_24h_change_24h: number;
};
};
}
const Container = styled.div`
padding: 0px 20px;
padding-top: 13vh;
max-width: 480px;
margin: 0 auto;
`;
const Title = styled.h1`
text-align: center;
padding-bottom: 5%;
font-size: 50px;
color: ${(props) => props.theme.accentColor};
`;
const Loader = styled.span`
display: block;
text-align: center;
`;
const Overview = styled.div`
display: flex;
justify-content: space-between;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px 20px;
border-radius: 10px;
`;
const OverviewItem = styled.div`
display: flex;
flex-direction: column;
align-items: center;
span:first-child {
font-size: 10px;
font-weight: 400;
text-transform: uppercase;
margin-bottom: 5px;
}
`;
const Description = styled.p`
margin: 20px 0px;
`;
const Tabs = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr);
margin: 25px 0px;
gap: 10px;
`;
const Tab = styled.span<{ isActive: boolean }>`
//props 받기
text-align: center;
text-transform: uppercase;
font-size: 12px;
font-weight: 400;
background-color: rgba(0, 0, 0, 0.5);
padding: 7px 0px;
border-radius: 10px;
color: ${(props) => (props.isActive ? props.theme.accentColor : props.theme.textColor)};
a {
display: block;
}
`;
export default Coin;
Chart.tsx import { useQuery } from 'react-query';
import { fetchCoinHistory } from './api';
import ApexChart from 'react-apexcharts';
import { useRecoilValue } from 'recoil';
import { isDarkAtom } from '../atoms';
interface IHistorycal {
time_open: string;
time_close: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
market_cap: number;
}
interface ChartProps {
coinId: string;
isDark: boolean;
}
function Chart({ coinId, isDark }: ChartProps) {
const { isLoading, data } = useQuery<IHistorycal[]>(
['ohlcv', coinId],
() => fetchCoinHistory(coinId),
{
refetchInterval: 10000,
}
);
//14개를 받아와야 하므로 배열로 전달.
return (
<div>
{isLoading ? (
'Loading chart...'
) : (
<ApexChart
type="line"
series={[
{
name: 'price',
data: data?.map((price) => price.close) as number[],
},
]}
options={{
theme: {
mode: isDark ? 'dark' : 'light',
},
chart: {
width: 500,
height: 300,
toolbar: { show: false },
background: 'transparents',
},
grid: { show: false },
stroke: { curve: 'smooth', width: 3 },
yaxis: { show: false },
xaxis: {
type: 'datetime',
categories: data?.map((price) => price.time_close),
},
fill: {
type: 'gradient',
gradient: { gradientToColors: ['#0be881'], stops: [0, 100] },
},
colors: ['blue'],
tooltip: { y: { formatter: (value) => `$ ${value.toFixed(3)}` } },
}}
/>
)}
</div>
);
}
export default Chart;
Header.tsx import styled from 'styled-components';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import BrightnessMediumIcon from '@mui/icons-material/BrightnessMedium';
import { useHistory } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { isDarkAtom } from '../atoms';
export default function Header() {
let history = useHistory();
const goBack = () => {
history.goBack();
};
const setterDarkAtom = useSetRecoilState(isDarkAtom);
const toggleDarkAtom = () => setterDarkAtom((prev) => !prev);
return (
<>
<Wrapper>
<Wrapper2>
<button onClick={goBack}>
<ArrowBackIosNewIcon sx={{ fontSize: 30, color: 'white' }} />
</button>
<span>Coin Tracker</span>
<button onClick={toggleDarkAtom}>
<BrightnessMediumIcon sx={{ fontSize: 30, color: 'white' }} />
</button>
</Wrapper2>
</Wrapper>
</>
);
}
const Wrapper = styled.div`
background-color: #1f2b38;
position: fixed;
width: 100vw;
height: 10vh;
`;
const Wrapper2 = styled.div`
background-color: #1f2b38;
max-width: 500px;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 auto;
span {
font-weight: bold;
color: ${(props) => props.theme.accentColor};
}
`;
atom.ts import { atom } from 'recoil';
export const isDarkAtom = atom({
key: 'isDark',
default: false,
});
Reference
この問題について(State Management 5.4_ Recap), 我々は、より多くの情報をここで見つけました
https://velog.io/@angel_eugnen/State-Management-5.4-Recap
テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { QueryClient, QueryClientProvider } from 'react-query';
import { RecoilRoot } from 'recoil';
const queryClient = new QueryClient();
ReactDOM.render(
<React.StrictMode>
<RecoilRoot>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</RecoilRoot>
</React.StrictMode>,
document.getElementById('root')
);
import styled from 'styled-components';
import { Link } from 'react-router-dom';
import { useQuery } from 'react-query';
import { fetchCoins } from './api';
import { Helmet } from 'react-helmet';
import Header from './Header';
const Container = styled.div`
padding: 0px 20px;
max-width: 480px;
margin: 0 auto;
`;
const CoinsList = styled.ul`
padding-top: 13vh;
font-weight: bold;
font-size: 20px;
`;
const Coin = styled.li`
background-color: white;
color: ${(props) => props.theme.bgColor};
margin-bottom: 10px;
padding: 20px;
border-radius: 15px;
a {
padding: 5px; // 좀 더 넓은 범위에서 transition 효과 적용 가능
transition: color 0.2s ease-in;
}
&:hover {
a {
color: ${(props) => props.theme.accentColor};
}
// 아래에서는 a가 아닌 Link라는 이름으로 사용했지만
// css에서는 anchor 를 선택해야 했다. 이건 모든 react router link들이
// 결국에는 anchor로 바뀔거기도 하고,
// react router dom이 우리 대신 설정을 도와줄 특별한 event listener들이 있기도 하다
}
`;
const Loader = styled.span`
text-align: center;
display: block;
`;
const Img = styled.img`
width: 25px;
height: 25px;
margin-right: 10px;
`;
interface ICoin {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
}
function Coins() {
const { isLoading, data } = useQuery<ICoin[]>('allCoins', fetchCoins);
console.log(isLoading, data);
return (
<>
<Header />
<Container>
<Helmet>
<title>코인</title>
</Helmet>
{isLoading ? (
<Loader>"Loading..."</Loader>
) : (
//loading 이 참이면 Loading... 출력, 거짓이면 CoinsList 보여줌
<CoinsList>
{data?.slice(0, 100).map((coin) => (
<Coin key={coin.id}>
<Img
src={`https://raw.githubusercontent.com/ErikThiart/cryptocurrency-icons/master/16/${coin.name
.toLowerCase()
.split(' ')
.join('-')}.png`}
/>
<Link
to={{
pathname: `/${coin.id}`,
state: { name: coin.name },
//Link를 이용해 string 이외에 더 많은 데이터를 보낼 수 있다
}}
>
{coin.id}
</Link>
</Coin>
))}
</CoinsList>
)}
</Container>
</>
);
}
export default Coins;
Coins.tsx import { useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { Switch, Route, useLocation, useParams, useRouteMatch } from 'react-router';
import { Link } from 'react-router-dom';
import Header from './Header';
import styled from 'styled-components';
import Chart from './Chart';
import Price from './Price';
import { fetchCoinInfo, fetchCoinTickers } from './api';
import { Helmet } from 'react-helmet';
import { useHistory } from 'react-router-dom';
function Coin() {
const { coinId } = useParams<RouteParams>();
const { state } = useLocation<RouteState>();
const priceMatch = useRouteMatch('/:coinId/price');
const chartMatch = useRouteMatch('/:coinId/chart');
const { isLoading: infoLoading, data: infoData } = useQuery<InfoData>(['info', coinId], () =>
fetchCoinInfo(coinId)
);
const { isLoading: tickersLoading, data: tickerData } = useQuery<PriceData>(
['tickers', coinId],
() => fetchCoinTickers(coinId),
{
refetchInterval: 5000,
}
);
const loading = infoLoading || tickersLoading;
return (
<>
<Header />
<Container>
<Helmet>
<title>{state?.name ? state.name : loading ? 'Loading...' : infoData?.name}</title>
</Helmet>
<Title>{state?.name ? state.name : loading ? 'Loading...' : infoData?.name}</Title>
{loading ? (
<Loader>Loading...</Loader>
) : (
<>
<Overview>
<OverviewItem>
<span>Rank:</span>
<span>{infoData?.rank}</span>
</OverviewItem>
<OverviewItem>
<span>Symbol:</span>
<span>${infoData?.symbol}</span>
</OverviewItem>
<OverviewItem>
<span>Price:</span>
<span>{tickerData?.quotes.USD.price.toFixed(3)}</span>
</OverviewItem>
</Overview>
<Description>{infoData?.description}</Description>
<Overview>
<OverviewItem>
<span>Total Suply:</span>
<span>{tickerData?.total_supply}</span>
</OverviewItem>
<OverviewItem>
<span>Max Supply:</span>
<span>{tickerData?.max_supply}</span>
</OverviewItem>
</Overview>
<Tabs>
<Tab isActive={chartMatch !== null}>
<Link to={`/${coinId}/chart`}>Chart</Link>
</Tab>
<Tab isActive={priceMatch !== null}>
<Link to={`/${coinId}/price`}>Price</Link>
</Tab>
</Tabs>
{/* 다양한 URL 로 Switch 하기 */}
<Switch>
<Route path={`/${coinId}/price`}>
<Price coinId={coinId} />
</Route>
<Route path={`/${coinId}/chart`}>
<Chart coinId={coinId} isDark={false} />
</Route>
</Switch>
</>
)}
</Container>
</>
);
}
interface RouteState {
name: string;
}
interface RouteParams {
coinId: string;
}
interface InfoData {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
description: string;
message: string;
open_source: boolean;
started_at: string;
development_status: string;
hardware_wallet: boolean;
proof_type: string;
org_structure: string;
hash_algorithm: string;
first_data_at: string;
last_data_at: string;
}
interface PriceData {
id: string;
name: string;
symbol: string;
rank: number;
circulating_supply: number;
total_supply: number;
max_supply: number;
beta_value: number;
first_data_at: string;
last_updated: string;
quotes: {
USD: {
ath_date: string;
ath_price: number;
market_cap: number;
market_cap_change_24h: number;
percent_change_1h: number;
percent_change_1y: number;
percent_change_6h: number;
percent_change_7d: number;
percent_change_12h: number;
percent_change_15m: number;
percent_change_24h: number;
percent_change_30d: number;
percent_change_30m: number;
percent_from_price_ath: number;
price: number;
volume_24h: number;
volume_24h_change_24h: number;
};
};
}
const Container = styled.div`
padding: 0px 20px;
padding-top: 13vh;
max-width: 480px;
margin: 0 auto;
`;
const Title = styled.h1`
text-align: center;
padding-bottom: 5%;
font-size: 50px;
color: ${(props) => props.theme.accentColor};
`;
const Loader = styled.span`
display: block;
text-align: center;
`;
const Overview = styled.div`
display: flex;
justify-content: space-between;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px 20px;
border-radius: 10px;
`;
const OverviewItem = styled.div`
display: flex;
flex-direction: column;
align-items: center;
span:first-child {
font-size: 10px;
font-weight: 400;
text-transform: uppercase;
margin-bottom: 5px;
}
`;
const Description = styled.p`
margin: 20px 0px;
`;
const Tabs = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr);
margin: 25px 0px;
gap: 10px;
`;
const Tab = styled.span<{ isActive: boolean }>`
//props 받기
text-align: center;
text-transform: uppercase;
font-size: 12px;
font-weight: 400;
background-color: rgba(0, 0, 0, 0.5);
padding: 7px 0px;
border-radius: 10px;
color: ${(props) => (props.isActive ? props.theme.accentColor : props.theme.textColor)};
a {
display: block;
}
`;
export default Coin;
Chart.tsx import { useQuery } from 'react-query';
import { fetchCoinHistory } from './api';
import ApexChart from 'react-apexcharts';
import { useRecoilValue } from 'recoil';
import { isDarkAtom } from '../atoms';
interface IHistorycal {
time_open: string;
time_close: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
market_cap: number;
}
interface ChartProps {
coinId: string;
isDark: boolean;
}
function Chart({ coinId, isDark }: ChartProps) {
const { isLoading, data } = useQuery<IHistorycal[]>(
['ohlcv', coinId],
() => fetchCoinHistory(coinId),
{
refetchInterval: 10000,
}
);
//14개를 받아와야 하므로 배열로 전달.
return (
<div>
{isLoading ? (
'Loading chart...'
) : (
<ApexChart
type="line"
series={[
{
name: 'price',
data: data?.map((price) => price.close) as number[],
},
]}
options={{
theme: {
mode: isDark ? 'dark' : 'light',
},
chart: {
width: 500,
height: 300,
toolbar: { show: false },
background: 'transparents',
},
grid: { show: false },
stroke: { curve: 'smooth', width: 3 },
yaxis: { show: false },
xaxis: {
type: 'datetime',
categories: data?.map((price) => price.time_close),
},
fill: {
type: 'gradient',
gradient: { gradientToColors: ['#0be881'], stops: [0, 100] },
},
colors: ['blue'],
tooltip: { y: { formatter: (value) => `$ ${value.toFixed(3)}` } },
}}
/>
)}
</div>
);
}
export default Chart;
Header.tsx import styled from 'styled-components';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import BrightnessMediumIcon from '@mui/icons-material/BrightnessMedium';
import { useHistory } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { isDarkAtom } from '../atoms';
export default function Header() {
let history = useHistory();
const goBack = () => {
history.goBack();
};
const setterDarkAtom = useSetRecoilState(isDarkAtom);
const toggleDarkAtom = () => setterDarkAtom((prev) => !prev);
return (
<>
<Wrapper>
<Wrapper2>
<button onClick={goBack}>
<ArrowBackIosNewIcon sx={{ fontSize: 30, color: 'white' }} />
</button>
<span>Coin Tracker</span>
<button onClick={toggleDarkAtom}>
<BrightnessMediumIcon sx={{ fontSize: 30, color: 'white' }} />
</button>
</Wrapper2>
</Wrapper>
</>
);
}
const Wrapper = styled.div`
background-color: #1f2b38;
position: fixed;
width: 100vw;
height: 10vh;
`;
const Wrapper2 = styled.div`
background-color: #1f2b38;
max-width: 500px;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 auto;
span {
font-weight: bold;
color: ${(props) => props.theme.accentColor};
}
`;
atom.ts import { atom } from 'recoil';
export const isDarkAtom = atom({
key: 'isDark',
default: false,
});
Reference
この問題について(State Management 5.4_ Recap), 我々は、より多くの情報をここで見つけました
https://velog.io/@angel_eugnen/State-Management-5.4-Recap
テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol
import { useEffect, useState } from 'react';
import { useQuery } from 'react-query';
import { Switch, Route, useLocation, useParams, useRouteMatch } from 'react-router';
import { Link } from 'react-router-dom';
import Header from './Header';
import styled from 'styled-components';
import Chart from './Chart';
import Price from './Price';
import { fetchCoinInfo, fetchCoinTickers } from './api';
import { Helmet } from 'react-helmet';
import { useHistory } from 'react-router-dom';
function Coin() {
const { coinId } = useParams<RouteParams>();
const { state } = useLocation<RouteState>();
const priceMatch = useRouteMatch('/:coinId/price');
const chartMatch = useRouteMatch('/:coinId/chart');
const { isLoading: infoLoading, data: infoData } = useQuery<InfoData>(['info', coinId], () =>
fetchCoinInfo(coinId)
);
const { isLoading: tickersLoading, data: tickerData } = useQuery<PriceData>(
['tickers', coinId],
() => fetchCoinTickers(coinId),
{
refetchInterval: 5000,
}
);
const loading = infoLoading || tickersLoading;
return (
<>
<Header />
<Container>
<Helmet>
<title>{state?.name ? state.name : loading ? 'Loading...' : infoData?.name}</title>
</Helmet>
<Title>{state?.name ? state.name : loading ? 'Loading...' : infoData?.name}</Title>
{loading ? (
<Loader>Loading...</Loader>
) : (
<>
<Overview>
<OverviewItem>
<span>Rank:</span>
<span>{infoData?.rank}</span>
</OverviewItem>
<OverviewItem>
<span>Symbol:</span>
<span>${infoData?.symbol}</span>
</OverviewItem>
<OverviewItem>
<span>Price:</span>
<span>{tickerData?.quotes.USD.price.toFixed(3)}</span>
</OverviewItem>
</Overview>
<Description>{infoData?.description}</Description>
<Overview>
<OverviewItem>
<span>Total Suply:</span>
<span>{tickerData?.total_supply}</span>
</OverviewItem>
<OverviewItem>
<span>Max Supply:</span>
<span>{tickerData?.max_supply}</span>
</OverviewItem>
</Overview>
<Tabs>
<Tab isActive={chartMatch !== null}>
<Link to={`/${coinId}/chart`}>Chart</Link>
</Tab>
<Tab isActive={priceMatch !== null}>
<Link to={`/${coinId}/price`}>Price</Link>
</Tab>
</Tabs>
{/* 다양한 URL 로 Switch 하기 */}
<Switch>
<Route path={`/${coinId}/price`}>
<Price coinId={coinId} />
</Route>
<Route path={`/${coinId}/chart`}>
<Chart coinId={coinId} isDark={false} />
</Route>
</Switch>
</>
)}
</Container>
</>
);
}
interface RouteState {
name: string;
}
interface RouteParams {
coinId: string;
}
interface InfoData {
id: string;
name: string;
symbol: string;
rank: number;
is_new: boolean;
is_active: boolean;
type: string;
description: string;
message: string;
open_source: boolean;
started_at: string;
development_status: string;
hardware_wallet: boolean;
proof_type: string;
org_structure: string;
hash_algorithm: string;
first_data_at: string;
last_data_at: string;
}
interface PriceData {
id: string;
name: string;
symbol: string;
rank: number;
circulating_supply: number;
total_supply: number;
max_supply: number;
beta_value: number;
first_data_at: string;
last_updated: string;
quotes: {
USD: {
ath_date: string;
ath_price: number;
market_cap: number;
market_cap_change_24h: number;
percent_change_1h: number;
percent_change_1y: number;
percent_change_6h: number;
percent_change_7d: number;
percent_change_12h: number;
percent_change_15m: number;
percent_change_24h: number;
percent_change_30d: number;
percent_change_30m: number;
percent_from_price_ath: number;
price: number;
volume_24h: number;
volume_24h_change_24h: number;
};
};
}
const Container = styled.div`
padding: 0px 20px;
padding-top: 13vh;
max-width: 480px;
margin: 0 auto;
`;
const Title = styled.h1`
text-align: center;
padding-bottom: 5%;
font-size: 50px;
color: ${(props) => props.theme.accentColor};
`;
const Loader = styled.span`
display: block;
text-align: center;
`;
const Overview = styled.div`
display: flex;
justify-content: space-between;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px 20px;
border-radius: 10px;
`;
const OverviewItem = styled.div`
display: flex;
flex-direction: column;
align-items: center;
span:first-child {
font-size: 10px;
font-weight: 400;
text-transform: uppercase;
margin-bottom: 5px;
}
`;
const Description = styled.p`
margin: 20px 0px;
`;
const Tabs = styled.div`
display: grid;
grid-template-columns: repeat(2, 1fr);
margin: 25px 0px;
gap: 10px;
`;
const Tab = styled.span<{ isActive: boolean }>`
//props 받기
text-align: center;
text-transform: uppercase;
font-size: 12px;
font-weight: 400;
background-color: rgba(0, 0, 0, 0.5);
padding: 7px 0px;
border-radius: 10px;
color: ${(props) => (props.isActive ? props.theme.accentColor : props.theme.textColor)};
a {
display: block;
}
`;
export default Coin;
import { useQuery } from 'react-query';
import { fetchCoinHistory } from './api';
import ApexChart from 'react-apexcharts';
import { useRecoilValue } from 'recoil';
import { isDarkAtom } from '../atoms';
interface IHistorycal {
time_open: string;
time_close: string;
open: number;
high: number;
low: number;
close: number;
volume: number;
market_cap: number;
}
interface ChartProps {
coinId: string;
isDark: boolean;
}
function Chart({ coinId, isDark }: ChartProps) {
const { isLoading, data } = useQuery<IHistorycal[]>(
['ohlcv', coinId],
() => fetchCoinHistory(coinId),
{
refetchInterval: 10000,
}
);
//14개를 받아와야 하므로 배열로 전달.
return (
<div>
{isLoading ? (
'Loading chart...'
) : (
<ApexChart
type="line"
series={[
{
name: 'price',
data: data?.map((price) => price.close) as number[],
},
]}
options={{
theme: {
mode: isDark ? 'dark' : 'light',
},
chart: {
width: 500,
height: 300,
toolbar: { show: false },
background: 'transparents',
},
grid: { show: false },
stroke: { curve: 'smooth', width: 3 },
yaxis: { show: false },
xaxis: {
type: 'datetime',
categories: data?.map((price) => price.time_close),
},
fill: {
type: 'gradient',
gradient: { gradientToColors: ['#0be881'], stops: [0, 100] },
},
colors: ['blue'],
tooltip: { y: { formatter: (value) => `$ ${value.toFixed(3)}` } },
}}
/>
)}
</div>
);
}
export default Chart;
Header.tsx import styled from 'styled-components';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import BrightnessMediumIcon from '@mui/icons-material/BrightnessMedium';
import { useHistory } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { isDarkAtom } from '../atoms';
export default function Header() {
let history = useHistory();
const goBack = () => {
history.goBack();
};
const setterDarkAtom = useSetRecoilState(isDarkAtom);
const toggleDarkAtom = () => setterDarkAtom((prev) => !prev);
return (
<>
<Wrapper>
<Wrapper2>
<button onClick={goBack}>
<ArrowBackIosNewIcon sx={{ fontSize: 30, color: 'white' }} />
</button>
<span>Coin Tracker</span>
<button onClick={toggleDarkAtom}>
<BrightnessMediumIcon sx={{ fontSize: 30, color: 'white' }} />
</button>
</Wrapper2>
</Wrapper>
</>
);
}
const Wrapper = styled.div`
background-color: #1f2b38;
position: fixed;
width: 100vw;
height: 10vh;
`;
const Wrapper2 = styled.div`
background-color: #1f2b38;
max-width: 500px;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 auto;
span {
font-weight: bold;
color: ${(props) => props.theme.accentColor};
}
`;
atom.ts import { atom } from 'recoil';
export const isDarkAtom = atom({
key: 'isDark',
default: false,
});
Reference
この問題について(State Management 5.4_ Recap), 我々は、より多くの情報をここで見つけました
https://velog.io/@angel_eugnen/State-Management-5.4-Recap
テキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol
import styled from 'styled-components';
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
import BrightnessMediumIcon from '@mui/icons-material/BrightnessMedium';
import { useHistory } from 'react-router-dom';
import { useSetRecoilState } from 'recoil';
import { isDarkAtom } from '../atoms';
export default function Header() {
let history = useHistory();
const goBack = () => {
history.goBack();
};
const setterDarkAtom = useSetRecoilState(isDarkAtom);
const toggleDarkAtom = () => setterDarkAtom((prev) => !prev);
return (
<>
<Wrapper>
<Wrapper2>
<button onClick={goBack}>
<ArrowBackIosNewIcon sx={{ fontSize: 30, color: 'white' }} />
</button>
<span>Coin Tracker</span>
<button onClick={toggleDarkAtom}>
<BrightnessMediumIcon sx={{ fontSize: 30, color: 'white' }} />
</button>
</Wrapper2>
</Wrapper>
</>
);
}
const Wrapper = styled.div`
background-color: #1f2b38;
position: fixed;
width: 100vw;
height: 10vh;
`;
const Wrapper2 = styled.div`
background-color: #1f2b38;
max-width: 500px;
height: 100%;
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 auto;
span {
font-weight: bold;
color: ${(props) => props.theme.accentColor};
}
`;
import { atom } from 'recoil';
export const isDarkAtom = atom({
key: 'isDark',
default: false,
});
Reference
この問題について(State Management 5.4_ Recap), 我々は、より多くの情報をここで見つけました https://velog.io/@angel_eugnen/State-Management-5.4-Recapテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol