From 43fa427a7532efa93341b1be6c229e261cd0c8a4 Mon Sep 17 00:00:00 2001 From: Alfex4936 <ikr@kakao.com> Date: Wed, 7 Jun 2023 12:51:08 +0900 Subject: [PATCH] feat: Finish the lecture --- docs/12_ko.html | 4 +- docs/14_ko.html | 2 +- docs/19_ko.html | 16 +- docs/20_ko.html | 10 +- docs/22_ko.html | 12 +- docs/24_ko.html | 26 +- docs/27_ko.html | 2 +- docs/29_ko.html | 12 +- docs/34_ko.html | 2 +- docs/35_ko.html | 2 +- docs/36_ko.html | 4 +- docs/37_ko.html | 2 +- docs/41_ko.html | 6 +- docs/42_ko.html | 2 +- docs/43_ko.html | 4 +- docs/49_ko.html | 2 +- docs/50_ko.html | 58 ++++- docs/51_ko.html | 112 +++++++++ docs/52_ko.html | 89 +++++++ docs/53_ko.html | 172 ++++++++++++++ docs/54_ko.html | 66 ++++++ docs/8080.png | Bin 0 -> 6981 bytes docs/TOC_ko.html | 6 +- docs/chapter_7_ko.html | 17 +- docs/weather.png | Bin 0 -> 22243 bytes frontend/generate.js | 11 +- frontend/lessons/ko/chapter_3.yaml | 2 +- frontend/lessons/ko/chapter_6.yaml | 368 ++++++++++++++++++++++++++++- frontend/lessons/ko/chapter_7.yaml | 24 +- package-lock.json | 65 ++--- package.json | 7 +- 31 files changed, 1004 insertions(+), 101 deletions(-) create mode 100644 docs/51_ko.html create mode 100644 docs/52_ko.html create mode 100644 docs/53_ko.html create mode 100644 docs/54_ko.html create mode 100644 docs/8080.png create mode 100644 docs/weather.png diff --git a/docs/12_ko.html b/docs/12_ko.html index 396188b..1b07bc9 100644 --- a/docs/12_ko.html +++ b/docs/12_ko.html @@ -40,8 +40,8 @@ <p>다행히 Rust는 <strong>as</strong> 키워드를 사용하여 숫자형을 쉽게 변환할 수 있습니다.</p> <p>또는 <code>parse</code>를 자주 사용합니다.</p> <pre><code class="rust">let my_string = "42"; -let my_integer = my_string.parse::<i32>().unwrap(); -// double colon op, ::<i32> syntax tells the compiler to parse the string as an i32 type</code></pre> +let my_integer = my_string.parse::<i32>().unwrap(); +// double colon op, ::<i32> syntax tells the compiler to parse the string as an i32 type</code></pre> <div class="bottomnav"> <span class="back"><a href="11_ko.html" rel="prev">❮ 이전</a></span> <span class="next"><a href="13_ko.html" rel="next">다음 ❯</a></span> diff --git a/docs/14_ko.html b/docs/14_ko.html index 0798fd9..f9763e1 100644 --- a/docs/14_ko.html +++ b/docs/14_ko.html @@ -87,7 +87,7 @@ fn main() { } number += 1; - if number > 20 { + if number > 20 { break 'search; } } diff --git a/docs/19_ko.html b/docs/19_ko.html index c65e14b..a882981 100644 --- a/docs/19_ko.html +++ b/docs/19_ko.html @@ -43,20 +43,20 @@ let number = 42; match number { - 0 => println!("숫자는 영입니다"), - 1 => println!("숫자는 일입니다"), - 42 => println!("인생, 우주, 그리고 모든 것에 대한 답"), - _ => println!("다른 숫자입니다"), + 0 => println!("숫자는 영입니다"), + 1 => println!("숫자는 일입니다"), + 42 => println!("인생, 우주, 그리고 모든 것에 대한 답"), + _ => println!("다른 숫자입니다"), } }</code></pre> <p>여기서는 number 변수의 값을 여러 패턴과 비교합니다.</p> <p><code>_</code> 패턴은 이전 패턴에서 명시적으로 다루지 않은 모든 값을 매치하는 <code>catch-all</code> 패턴입니다.</p> <pre><code class="rust">fn classify_age(age: u8) { match age { - 0..=12 => println!("어린이"), - 13..=19 => println!("청소년"), - 20..=64 => println!("성인"), - _ => println!("노인"), + 0..=12 => println!("어린이"), + 13..=19 => println!("청소년"), + 20..=64 => println!("성인"), + _ => println!("노인"), } } fn main() { diff --git a/docs/20_ko.html b/docs/20_ko.html index 1b61957..3c744c7 100644 --- a/docs/20_ko.html +++ b/docs/20_ko.html @@ -64,9 +64,9 @@ <p>열거형을 다루려면, 종종 match 표현식을 사용하며, 이를 통해 열거형 변종에 따라 다른 작업을 수행할 수 있습니다:</p> <pre><code class="rust">fn print_status_message(status: Status) { match status { - Status::Active => println!("사용자가 활성 상태입니다."), - Status::Inactive => println!("사용자가 비활성 상태입니다."), - Status::Pending => println!("사용자가 보류 중입니다."), + Status::Active => println!("사용자가 활성 상태입니다."), + Status::Inactive => println!("사용자가 비활성 상태입니다."), + Status::Pending => println!("사용자가 보류 중입니다."), } } fn main() { @@ -102,8 +102,8 @@ fn main() { // 튜플 열거형 변종의 값에 접근 match circle { - Shape::Circle(radius) => println!("원의 반지름: {}", radius), - _ => (), + Shape::Circle(radius) => println!("원의 반지름: {}", radius), + _ => (), } }</code></pre> <hr /> diff --git a/docs/22_ko.html b/docs/22_ko.html index 52701e3..f9d8a05 100644 --- a/docs/22_ko.html +++ b/docs/22_ko.html @@ -47,10 +47,10 @@ Rust에서는 결과를 나타내기 위해 Result 열거형을 사용합니다. <p>예를 들어, 정수를 문자열로 변환하는 간단한 함수를 작성해 봅시다.</p> <p>이 함수는 문자열을 입력으로 받아 정수로 변환하려고 시도하고, 변환에 성공하면 Ok 값을 반환합니다.</p> <p>만약 변환에 실패하면, Err 값을 반환합니다.</p> -<pre><code class="rust">fn parse_integer(input: &str) -> Result<i32, String> { - match input.parse::<i32>() { - Ok(value) => Ok(value), - Err(_) => Err(format!("'{}' is not a valid integer.", input)), +<pre><code class="rust">fn parse_integer(input: &str) -> Result<i32, String> { + match input.parse::<i32>() { + Ok(value) => Ok(value), + Err(_) => Err(format!("'{}' is not a valid integer.", input)), } }</code></pre> <p><strong>match 문을 사용한 오류 처리</strong></p> @@ -61,8 +61,8 @@ Rust에서는 결과를 나타내기 위해 Result 열거형을 사용합니다. let parsed = parse_integer(input); match parsed { - Ok(value) => println!("The integer value is: {}", value), - Err(error) => println!("Error: {}", error), + Ok(value) => println!("The integer value is: {}", value), + Err(error) => println!("Error: {}", error), } }</code></pre> <p>이 코드는 parse_integer 함수를 호출하여 결과를 가져옵니다.</p> diff --git a/docs/24_ko.html b/docs/24_ko.html index d7faa17..98a9df4 100644 --- a/docs/24_ko.html +++ b/docs/24_ko.html @@ -52,11 +52,11 @@ } // 넓이를 계산하는 함수 -fn area(shape: &Shape) -> f64 { +fn area(shape: &Shape) -> f64 { match shape { - Shape::Circle(r) => 3.14 * r * r, - Shape::Rectangle(w, h) => w * h, - Shape::Triangle(a, b, c) => { + Shape::Circle(r) => 3.14 * r * r, + Shape::Rectangle(w, h) => w * h, + Shape::Triangle(a, b, c) => { // 삼각형의 넓이 공식 let s = (a + b + c) / 2.0; let area = s * (s - a) * (s - b) * (s - c); @@ -71,15 +71,15 @@ fn area(shape: &Shape) -> f64 { } // 둘레를 계산하는 함수 -fn perimeter(shape: &Shape) -> f64 { +fn perimeter(shape: &Shape) -> f64 { match shape { - Shape::Circle(r) => 2.0 * 3.14 * r, - Shape::Rectangle(w, h) => 2.0 * (w + h), - Shape::Triangle(a, b, c) => { + Shape::Circle(r) => 2.0 * 3.14 * r, + Shape::Rectangle(w, h) => 2.0 * (w + h), + Shape::Triangle(a, b, c) => { // 삼각형의 둘레 공식 let p = a + b + c; // 둘레가 음수면 에러 발생 - if p < 0.0 { + if p < 0.0 { panic!("Invalid triangle"); } else { p @@ -89,11 +89,11 @@ fn perimeter(shape: &Shape) -> f64 { } // 정사각형인지 판별하는 함수 -fn is_square(shape: &Shape) -> bool { +fn is_square(shape: &Shape) -> bool { match shape { - Shape::Circle(_) => false, - Shape::Rectangle(w, h) => w == h, - Shape::Triangle(_, _, _) => false, + Shape::Circle(_) => false, + Shape::Rectangle(w, h) => w == h, + Shape::Triangle(_, _, _) => false, } } diff --git a/docs/27_ko.html b/docs/27_ko.html index 341c603..0080735 100644 --- a/docs/27_ko.html +++ b/docs/27_ko.html @@ -70,7 +70,7 @@ fn main() { <h2 id="clone"><code>clone</code> 메서드를 사용한 소유권 이전 방지</h2> <pre><code class="rust">let s1 = String::from("hello"); let s2 = s1.clone();</code></pre> <h2 id="">함수로 소유권 이전 후 반환</h2> -<pre><code class="rust">fn takes_and_gives_back(s: String) -> String { +<pre><code class="rust">fn takes_and_gives_back(s: String) -> String { s } diff --git a/docs/29_ko.html b/docs/29_ko.html index 7a85462..c4615ef 100644 --- a/docs/29_ko.html +++ b/docs/29_ko.html @@ -49,8 +49,8 @@ <p>예제 1: 함수 시그니처에서 수명 표시</p> <pre><code class="rust">// 여기에서 사용된 'a는 수명을 나타내는 표시입니다. // 이를 통해 입력과 출력의 참조들이 동일한 수명을 가지도록 합니다. -fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str { - if s1.len() > s2.len() { +fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str { + if s1.len() > s2.len() { s1 } else { s2 @@ -59,7 +59,7 @@ fn longest<'a>(s1: &'a str, s2: &'a str) -> &'a str { <p>예제 2: 구조체에서 수명 표시</p> <pre><code class="rust">// Person 구조체는 이름을 문자열 슬라이스로 저장합니다. // 여기에서 사용된 'a는 구조체의 이름 필드가 참조하는 문자열 슬라이스의 수명을 나타냅니다. -struct Person<'a> { +struct Person<'a> { name: &'a str, }</code></pre> <p>수명과 빌림 검사기:</p> @@ -68,12 +68,12 @@ struct Person<'a> { <p>그러나 복잡한 상황에서는 개발자가 수명을 명시해야 할 수도 있습니다.</p> <p>수명을 이해하고 올바르게 사용함으로써 Rust의 빌림 검사기가 메모리 관리를 안전하게 수행할 수 있도록 지원할 수 있습니다.</p> <p>이는 Rust 프로그램의 성능과 안정성에 크게 기여합니다.</p> -<pre><code class="rust">struct Wrapper<'a, T: 'a> { +<pre><code class="rust">struct Wrapper<'a, T: 'a> { value: &'a T, } -impl<'a, T> Wrapper<'a, T> { - fn new(value: &'a T) -> Self { +impl<'a, T> Wrapper<'a, T> { + fn new(value: &'a T) -> Self { Wrapper { value } } } diff --git a/docs/34_ko.html b/docs/34_ko.html index bd18db6..7ac0805 100644 --- a/docs/34_ko.html +++ b/docs/34_ko.html @@ -41,7 +41,7 @@ <h2 id="a">a. 벡터 생성 및 초기화</h2> <p>벡터를 생성하려면 다음과 같은 방법을 사용할 수 있습니다.</p> <pre><code class="rust">// 빈 벡터 생성 -let mut vec1: Vec<i32> = Vec::new(); +let mut vec1: Vec<i32> = Vec::new(); // 초기 값이 있는 벡터 생성 let vec2 = vec![1, 2, 3, 4, 5];</code></pre> diff --git a/docs/35_ko.html b/docs/35_ko.html index 2770095..5910688 100644 --- a/docs/35_ko.html +++ b/docs/35_ko.html @@ -61,7 +61,7 @@ println!("변경된 벡터: {:?}", vec);</code></pre> <p>이를 통해 벡터의 요소를 다른 데이터 구조로 쉽게 옮길 수 있습니다.</p> <pre><code class="rust">let vec = vec!["a".to_string(), "b".to_string(), "c".to_string()]; -let mut uppercased_vec: Vec<String> = Vec::new(); +let mut uppercased_vec: Vec<String> = Vec::new(); for element in vec.into_iter() { uppercased_vec.push(element.to_uppercase()); diff --git a/docs/36_ko.html b/docs/36_ko.html index e33040c..1344bae 100644 --- a/docs/36_ko.html +++ b/docs/36_ko.html @@ -44,13 +44,13 @@ <pre><code class="rust">use std::collections::HashMap; // 빈 해시맵 생성 -let mut scores: HashMap<String, u32> = HashMap::new(); +let mut scores: HashMap<String, u32> = HashMap::new(); // 초기 값이 있는 해시맵 생성 let scores = vec![("Alice", 50), ("Bob", 60)] .into_iter() .map(|(k, v)| (k.to_string(), v)) - .collect::<HashMap<String, u32>>();</code></pre> + .collect::<HashMap<String, u32>>();</code></pre> <h2 id="b">b. 키-값 쌍 삽입 및 업데이트</h2> <p>해시맵에 키-값 쌍을 삽입하거나 업데이트하려면 <code>insert</code> 메서드를 사용합니다.</p> <pre><code class="rust">// 키-값 쌍 삽입 diff --git a/docs/37_ko.html b/docs/37_ko.html index 9bc2b3a..e21df65 100644 --- a/docs/37_ko.html +++ b/docs/37_ko.html @@ -62,7 +62,7 @@ while let Some(number) = numbers.next() { <h3 id="collect">collect</h3> <p><code>collect</code> 메서드는 이터레이터의 요소를 다른 컬렉션 타입으로 변환합니다.</p> <pre><code class="rust">let numbers = vec![1, 2, 3, 4, 5]; -let doubled_numbers: Vec<_> = numbers.iter().map(|x| x * 2).collect(); +let doubled_numbers: Vec<_> = numbers.iter().map(|x| x * 2).collect(); println!("Doubled numbers: {:?}", doubled_numbers);</code></pre> <p>이 외에도 다양한 이터레이터 메서드가 있습니다.</p> diff --git a/docs/41_ko.html b/docs/41_ko.html index 8953390..5df52f8 100644 --- a/docs/41_ko.html +++ b/docs/41_ko.html @@ -45,7 +45,7 @@ <p>선언형 매크로는 매크로 규칙을 사용하여 코드를 생성하는 매크로입니다.</p> <p><code>macro_rules!</code> 키워드를 사용하여 선언형 매크로를 정의할 수 있습니다.</p> <pre><code class="rust">macro_rules! vec { - ( $( $x:expr ),* ) => { + ( $( $x:expr ),* ) => { { let mut temp_vec = Vec::new(); $( @@ -70,7 +70,7 @@ use quote::quote; use syn::{parse_macro_input, DeriveInput}; #[proc_macro_derive(HelloMacro)] -pub fn hello_macro_derive(input: TokenStream) -> TokenStream { +pub fn hello_macro_derive(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let name = &ast.ident; let gen = quote! { @@ -112,7 +112,7 @@ fn main() { <li><code>$(...)+</code>: 1회 이상 반복</li> </ul> <pre><code class="rust">macro_rules! create_function { - ($func_name:ident) => ( + ($func_name:ident) => ( fn $func_name() { println!("함수 {}가 호출되었습니다.", stringify!($func_name)); } diff --git a/docs/42_ko.html b/docs/42_ko.html index 84562b2..ab59b54 100644 --- a/docs/42_ko.html +++ b/docs/42_ko.html @@ -75,7 +75,7 @@ fn main() { <p>외부 함수를 사용하려면 <code>extern</code> 키워드와 <code>unsafe</code>를 사용해야 합니다.</p> <pre><code class="rust">// C 언어의 함수를 호출하는 예시 extern "C" { - fn abs(input: i32) -> i32; + fn abs(input: i32) -> i32; } fn main() { diff --git a/docs/43_ko.html b/docs/43_ko.html index a29cdc2..d5fddd9 100644 --- a/docs/43_ko.html +++ b/docs/43_ko.html @@ -47,7 +47,7 @@ </ol> <div> <button type="button" class="collapsible">정답 보기</button> <div class="content"> <p> <pre><code class="rust">// 매크로를 사용하여 두 벡터의 덧셈을 수행하는 함수를 작성합니다. macro_rules! add_vectors { - ($vec1:expr, $vec2:expr) => { + ($vec1:expr, $vec2:expr) => { add_vectors_unsafe(&$vec1, &$vec2) }; } @@ -60,7 +60,7 @@ fn main() { println!("벡터 덧셈 결과: {:?}", result); } -fn add_vectors_unsafe(vec1: &[i32], vec2: &[i32]) -> Vec<i32> { +fn add_vectors_unsafe(vec1: &[i32], vec2: &[i32]) -> Vec<i32> { // 벡터의 길이가 같은지 확인합니다. assert_eq!(vec1.len(), vec2.len()); diff --git a/docs/49_ko.html b/docs/49_ko.html index 4b2c551..7114304 100644 --- a/docs/49_ko.html +++ b/docs/49_ko.html @@ -42,7 +42,7 @@ <p><img src="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbGZOnJ%2Fbtra8y6n31j%2FZyIuDLgVyQPqzknNrIXqSk%2Fimg.png" alt="BasicCard" /></p> <h1 id="">준비물:</h1> <ol> -<li>public에서 접속 가능한 주소 (포트포워딩이나 AWS EC2를 이용)</li> +<li>public에서 접속 가능한 머신 (포트포워딩이나 AWS EC2, <a href="https://fly.io" target="_blank" rel="noopener">fly.io</a> 등 이용)</li> <li>카카오 i 챗봇 만들기 <a href="https://i.kakao.com/" target="_blank" rel="noopener">@링크</a></li> <li>카카오톡 채널 만들기 <a href="https://center-pf.kakao.com/profiles" target="_blank" rel="noopener">@링크</a></li> </ol> diff --git a/docs/50_ko.html b/docs/50_ko.html index 4c51834..963341c 100644 --- a/docs/50_ko.html +++ b/docs/50_ko.html @@ -35,14 +35,64 @@ <span class="toc"><a href="TOC_ko.html">목차</a></span> </div> <div class="page"> - <h1>Chapter 6 - Conclusion</h1> - <p>CSW</p> + <h1>Hello World API</h1> + <p>이번 튜토리얼에서는 Rust와 Actix-web을 이용하여 'Hello World' 메시지를 출력하는 기본적인 웹 서버를 만들어 보겠습니다.</p> +<h2 id="">시작하기</h2> +<p>첫 단계로, 새로운 binary 기반의 Cargo 프로젝트를 생성합니다:</p> +<pre><code class="bash">cargo new hello-world +cd hello-world</code></pre> +<p>그 후, 프로젝트에 actix-web을 의존성으로 추가해야 합니다.</p> +<p>이를 위해 <code>Cargo.toml</code> 파일을 열고 다음과 같이 입력합니다:</p> +<pre><code class="toml">[dependencies] +actix-web = "4"</code></pre> +<h2 id="-1">핸들러 작성하기</h2> +<p>웹 서버에서 요청을 처리하기 위해 핸들러 함수를 작성합니다.</p> +<p>이 함수들은 비동기 함수로, 필요한 매개변수를 받아 HttpResponse를 반환합니다.</p> +<p>이 HttpResponse는 웹 서버가 클라이언트에게 보낼 응답입니다.</p> +<p><code>src/main.rs</code> 파일을 다음과 같이 수정합니다:</p> +<pre><code class="rust">use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder}; + +#[get("/")] +async fn hello() -> impl Responder { + HttpResponse::Ok().body("Hello world!") +} + +#[post("/echo")] +async fn echo(req_body: String) -> impl Responder { + HttpResponse::Ok().body(req_body) +} + +async fn manual_hello() -> impl Responder { + HttpResponse::Ok().body("Hey there!") +}</code></pre> +<p>각 핸들러는 HTTP 메소드와 경로에 따라 요청을 처리합니다.</p> +<p>수동으로 경로를 설정하고 싶다면, 그에 맞는 함수를 작성하면 됩니다.</p> +<h2 id="app">App 생성 및 요청 핸들러 등록</h2> +<p>다음 단계로, App 인스턴스를 생성하고 요청 핸들러를 등록합니다.</p> +<p>경로 정보가 있는 핸들러는 <code>App::service</code>를 사용하고, 수동으로 경로를 설정한 핸들러는 <code>App::route</code>를 사용합니다.</p> +<pre><code class="rust">#[actix_web::main] +async fn main() -> std::io::Result<()> { + HttpServer::new(|| { + App::new() + .service(hello) + .service(echo) + .route("/hey", web::get().to(manual_hello)) + }) + .bind(("127.0.0.1", 8080))? + .run() + .await +}</code></pre> +<h2 id="-2">서버 실행</h2> +<p>이제 <code>cargo run</code> 명령어를 통해 프로그램을 컴파일하고 실행할 수 있습니다.</p> +<p>웹 브라우저에서 http://127.0.0.1:8080/ 주소로 접속하면 'Hello World' 메시지를 확인할 수 있습니다.</p> +<p>이 간단한 예제를 통해 Rust와 Actix-web을 이용하여 웹 서버를 어떻게 만드는지 배웠습니다.</p> +<p>이러한 기본 원리를 이용하면 다양한 웹 서비스를 만들어볼 수 있습니다.</p> <div class="bottomnav"> <span class="back"><a href="49_ko.html" rel="prev">❮ 이전</a></span> - <span class="next"><a href="chapter_7_ko.html" rel="next">다음 ❯</a></span> + <span class="next"><a href="51_ko.html" rel="next">다음 ❯</a></span> </div> </div> - <div class="code"><center><img src="/ferris_lofi.png" alt="Rust Tutorial" width="80%" height="100%"></center></div> + <div class="code"><center><img src="/8080.png" alt="Rust Tutorial" width="80%" height="100%"></center></div> </div> <script> var pres = document.querySelectorAll("pre>code"); diff --git a/docs/51_ko.html b/docs/51_ko.html new file mode 100644 index 0000000..beb6dfd --- /dev/null +++ b/docs/51_ko.html @@ -0,0 +1,112 @@ +<!DOCTYPE html> + <html lang="ko"> + <head> + <title>Rust 튜토리얼 - 자기주도프로젝트</title> + + <meta charset="UTF-8"> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2"> + <meta name="keywords" content="Rust, Programming, Learning"> + <meta name="description" content="Rust tutorial website based on tour_of_rust by 최석원"> + <meta name="theme-color" content="#ff6801"/> + <meta http-equiv="Cache-Control" content="max-age=3600"> + + <link rel="stylesheet" href="tour.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/night-owl.min.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.1/css/all.min.css"> + + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> + <link rel="/manifest" href="./site.webmanifest"> + + <script src="//unpkg.com/@highlightjs/cdn-assets@11.7.0/highlight.min.js"></script> + + <script src="./tour.js" defer></script> + <!-- <script>hljs.highlightAll();</script> --> + <script src="./highlight.badge.min.js"></script> + </head> + <body> + <div class="tour"> + <div class="header"> + <span class="title"><a href="index.html">Rust 튜토리얼</a></span> + <span class="nav"> + <span class="toc"><a href="TOC_ko.html">목차</a></span> + </div> + <div class="page"> + <h1>main() && impl Responder</h1> + <h2 id="actixrsmain">actix-rs main()</h2> +<p>아래는 웹 서버를 시작하는 역할을 합니다. </p> +<pre><code class="rust">#[actix_web::main] +async fn main() -> std::io::Result<()> { + HttpServer::new(|| { + App::new() + .service(hello) + .service(echo) + .route("/hey", web::get().to(manual_hello)) + }) + .bind(("127.0.0.1", 8080))? // localhost:8080 or 127.0.0.1:8080 + .run() + .await +}</code></pre> +<p>여기서 <code>#[actix_web::main]</code>은 Actix 웹 프레임워크에서 제공하는 매크로로, 이를 이용해 비동기 메인 함수를 생성할 수 있습니다. </p> +<p>그리고 <code>HttpServer::new</code> 함수를 호출하여 새로운 HTTP 서버 인스턴스를 만듭니다.</p> +<p>이 함수의 인자로는 클로저가 들어가며, 이 클로저 내부에서 App 인스턴스를 만들고,</p> +<p>우리가 앞서 정의한 hello, echo, 그리고 manual_hello 함수를 각각 서비스로 등록합니다.</p> +<p>또한, <code>/hey</code> 라는 경로는 수동으로 설정된 경로입니다.</p> +<p><code>web::get().to(manual_hello)</code>를 통해 get 요청이 들어왔을 때 <code>manual_hello</code> 함수가 호출되도록 설정했습니다.</p> +<p>그 후 <code>.bind(("127.0.0.1", 8080))?</code>를 통해 서버를 로컬 호스트의 8080 포트에 바인딩하게 됩니다.</p> +<p>만약 바인딩에 실패하면 에러를 반환하며 프로그램은 종료됩니다.</p> +<p>마지막으로 <code>.run().await</code>를 통해 서버를 실행시키며, 이 서버는 비동기적으로 작동하게 됩니다.</p> +<h2 id="implresponder">impl Responder</h2> +<p>Rust의 'Trait'에 대해 알아보고, 특히 actix-web에서 제공하는 'Responder' Trait에 대해 살펴보겠습니다.</p> +<p>Responder는 웹 응답을 만드는데 굉장히 중요한 역할을 하는 Trait입니다.</p> +<h2 id="trait">Trait 이란?</h2> +<p>Rust에서 Trait는 특정 기능이나 행동을 정의한 것으로,</p> +<p>Trait를 구현하면 해당 구조체나 열거형은 Trait에서 정의된 메소드를 사용할 수 있게 됩니다.</p> +<p>Rust는 상속 대신 Trait를 사용하여 코드의 재사용성과 모듈성을 증가시킵니다.</p> +<h2 id="respondertrait">Responder Trait</h2> +<p>Responder는 actix-web에서 제공하는 Trait 중 하나로, HTTP 응답을 생성하는 메소드를 제공합니다.</p> +<p>이 Trait를 이용하면 웹 서버가 클라이언트에게 보내는 HTTP 응답을 쉽게 만들 수 있습니다. </p> +<p>Responder Trait은 두 가지 메소드를 정의합니다: </p> +<ol> +<li><code>respond_to</code>: HttpRequest 객체를 받아 HttpResponse를 생성하는 메소드로, 이는 핸들러에서 클라이언트의 요청을 받아 적절한 응답을 생성하는 데 사용됩니다.</li> +<li><code>customize</code>: 응답을 커스터마이징 할 수 있는 메소드로, 이 메소드는 Responder가 구현되어 있는 경우 사용할 수 있습니다.</li> +</ol> +<h2 id="responder">Responder 사용하기</h2> +<p>핸들러 함수에서는 <code>impl Responder</code>를 리턴 타입으로 사용합니다.</p> +<p>이렇게 하면 어떤 값이든, 그 값이 Responder Trait를 구현하고 있다면 리턴할 수 있게 됩니다.</p> +<p>예를 들어, 'String', 'HttpResponse' 등은 모두 Responder를 구현하고 있습니다.</p> +<p>이를 통해 해당 값을 리턴하는 핸들러를 쉽게 만들 수 있습니다.</p> +<p>Rust의 강력한 타입 시스템 덕분에 컴파일 타임에 각 핸들러가 어떤 타입의 값을 리턴하는지를 확인할 수 있게 되어,</p> +<p>런타임 에러를 사전에 방지하는 데 매우 유용합니다.</p> +<p>결국, Responder는 웹 응답을 만드는 과정을 추상화하고, 다양한 타입의 값을 HTTP 응답으로 쉽게 변환할 수 있게 해주는 역할을 합니다.</p> +<p>이를 통해 강력하면서도 유연한 웹 응답을 만들 수 있게 됩니다.</p> +<p>Responder를 활용하면 웹 서버 개발이 훨씬 편리해집니다.</p> + <div class="bottomnav"> + <span class="back"><a href="50_ko.html" rel="prev">❮ 이전</a></span> + <span class="next"><a href="52_ko.html" rel="next">다음 ❯</a></span> + </div> + </div> + <div class="code"><center><img src="/8080.png" alt="Rust Tutorial" width="80%" height="100%"></center></div> + </div> + <script> + var pres = document.querySelectorAll("pre>code"); + for (var i = 0; i < pres.length; i++) { + hljs.highlightElement(pres[i]); + } + var options = { + loadDelay: 0, + copyIconClass: "far fa-clipboard", + checkIconClass: "fa fa-check text-success", + blogURL: "http://rust-study.ajousw.kr/" + }; + window.highlightJsBadge(options); + </script> + + <footer> + <p><a target="_blank" rel="noopener" href="https://www.youtube.com/c/SoftwareToolTime">아주대학교 Software Tool Time</a> - Rust 튜토리얼 (Basic)</p> + </footer> + </body> +</html> \ No newline at end of file diff --git a/docs/52_ko.html b/docs/52_ko.html new file mode 100644 index 0000000..e7b96a8 --- /dev/null +++ b/docs/52_ko.html @@ -0,0 +1,89 @@ +<!DOCTYPE html> + <html lang="ko"> + <head> + <title>Rust 튜토리얼 - 자기주도프로젝트</title> + + <meta charset="UTF-8"> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2"> + <meta name="keywords" content="Rust, Programming, Learning"> + <meta name="description" content="Rust tutorial website based on tour_of_rust by 최석원"> + <meta name="theme-color" content="#ff6801"/> + <meta http-equiv="Cache-Control" content="max-age=3600"> + + <link rel="stylesheet" href="tour.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/night-owl.min.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.1/css/all.min.css"> + + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> + <link rel="/manifest" href="./site.webmanifest"> + + <script src="//unpkg.com/@highlightjs/cdn-assets@11.7.0/highlight.min.js"></script> + + <script src="./tour.js" defer></script> + <!-- <script>hljs.highlightAll();</script> --> + <script src="./highlight.badge.min.js"></script> + </head> + <body> + <div class="tour"> + <div class="header"> + <span class="title"><a href="index.html">Rust 튜토리얼</a></span> + <span class="nav"> + <span class="toc"><a href="TOC_ko.html">목차</a></span> + </div> + <div class="page"> + <h1>kakao-rs</h1> + <p>카카오톡 API 템플릿을 쉽게 만들어 주는 <code>kakao-rs</code> 라이브러리에 대해 알아보겠습니다. </p> +<h2 id="kakaors">kakao-rs 라이브러리란?</h2> +<p>kakao-rs는 Rust 언어로 작성된 카카오 챗봇 서버를 만들 때 사용할 수 있는 라이브러리입니다.</p> +<p>이 라이브러리는 SimpleText, SimpleImage, ListCard, Carousel, BasicCard, CommerceCard, ItemCard 등의 JSON 데이터를 쉽게 생성할 수 있도록 돕는 도구들을 제공합니다.</p> +<h2 id="">사용 방법</h2> +<p>kakao-rs 라이브러리를 사용하려면, 먼저 프로젝트의 <code>Cargo.toml</code> 파일에 kakao-rs를 의존성으로 추가해야 합니다. </p> +<pre><code class="toml">[dependencies] +kakao-rs = "0.3"</code></pre> +<p>이 라이브러리를 이용하면 다양한 종류의 버튼(예: 공유 버튼, 링크 버튼, 일반 메시지 버튼, 전화 버튼 등)을 쉽게 만들 수 있습니다.</p> +<h2 id="json">카카오 JSON 데이터 연동</h2> +<p>kakao-rs는 카카오 JSON 데이터와의 연동이 매우 간단합니다.</p> +<p>유저의 발화문을 얻기 위해서는 아래와 같이 작성하면 됩니다.</p> +<pre><code class="rust">#[post("/end")] +pub async fn test(kakao: web::Json<Value>) -> impl Responder { // actix + println!("{}", kakao["userRequest"]["utterance"].as_str().unwrap()); // 발화문 + unimplemented!() +}</code></pre> +<p>이 라이브러리를 이용하면 다양한 형태의 카카오 챗봇 메시지를 쉽게 생성할 수 있습니다.</p> +<p>예를 들어, ListCard를 생성하는 코드는 아래와 같습니다.</p> +<pre><code class="rust">let mut list_card = ListCard::new("리스트 카드 제목!"); // 제목 +// ... +result.add_output(list_card.build()); // moved list_card's ownership</code></pre> +<p>kakao-rs 라이브러리를 통해 SimpleText, SimpleImage, BasicCard, CommerceCard, Carousel 등의</p> +<p>다양한 형태의 카카오 챗봇 메시지를 쉽게 생성할 수 있습니다.</p> +<p>카카오 챗봇 서버를 Rust로 구현하려는 개발자들에게 kakao-rs 라이브러리는 매우 유용한 도구가 될 것입니다.</p> + <div class="bottomnav"> + <span class="back"><a href="51_ko.html" rel="prev">❮ 이전</a></span> + <span class="next"><a href="53_ko.html" rel="next">다음 ❯</a></span> + </div> + </div> + <div class="code"><center><img src="/ferris_lofi.png" alt="Rust Tutorial" width="80%" height="100%"></center></div> + </div> + <script> + var pres = document.querySelectorAll("pre>code"); + for (var i = 0; i < pres.length; i++) { + hljs.highlightElement(pres[i]); + } + var options = { + loadDelay: 0, + copyIconClass: "far fa-clipboard", + checkIconClass: "fa fa-check text-success", + blogURL: "http://rust-study.ajousw.kr/" + }; + window.highlightJsBadge(options); + </script> + + <footer> + <p><a target="_blank" rel="noopener" href="https://www.youtube.com/c/SoftwareToolTime">아주대학교 Software Tool Time</a> - Rust 튜토리얼 (Basic)</p> + </footer> + </body> +</html> \ No newline at end of file diff --git a/docs/53_ko.html b/docs/53_ko.html new file mode 100644 index 0000000..dac6ab8 --- /dev/null +++ b/docs/53_ko.html @@ -0,0 +1,172 @@ +<!DOCTYPE html> + <html lang="ko"> + <head> + <title>Rust 튜토리얼 - 자기주도프로젝트</title> + + <meta charset="UTF-8"> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2"> + <meta name="keywords" content="Rust, Programming, Learning"> + <meta name="description" content="Rust tutorial website based on tour_of_rust by 최석원"> + <meta name="theme-color" content="#ff6801"/> + <meta http-equiv="Cache-Control" content="max-age=3600"> + + <link rel="stylesheet" href="tour.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/night-owl.min.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.1/css/all.min.css"> + + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> + <link rel="/manifest" href="./site.webmanifest"> + + <script src="//unpkg.com/@highlightjs/cdn-assets@11.7.0/highlight.min.js"></script> + + <script src="./tour.js" defer></script> + <!-- <script>hljs.highlightAll();</script> --> + <script src="./highlight.badge.min.js"></script> + </head> + <body> + <div class="tour"> + <div class="header"> + <span class="title"><a href="index.html">Rust 튜토리얼</a></span> + <span class="nav"> + <span class="toc"><a href="TOC_ko.html">목차</a></span> + </div> + <div class="page"> + <h1>실행</h1> + <p><code>kakao-rs</code>를 <code>actix-rs</code>에 적용시켜보겠습니다.</p> +<p>단순하게 SimpleText를 반환할 것이며 더 많은 카카오톡 반응 디자인은 다음을 참고하세요: <a href="https://i.kakao.com/docs/skill-response-format" target="_blank" rel="noopener">@링크</a></p> +<div align="center"> +<p> + <img src="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn5Ud0%2FbtraVUpTmyM%2FLSdvhi5uKkzx9tcN2SFbh1%2Fimg.png"> +</p> +</div> +<ol> +<li>의존성 추가</li> +</ol> +<pre><code class="bash">cargo add kakao-rs</code></pre> +<p>카카오톡 챗봇 POST json 구조: 챗봇 관리자 > 스킬 > 편집 </p> +<pre><code class="json">"intent": { + "id": "hequ", + "name": "블록 이름" + }, + "userRequest": { + "timezone": "Asia/Seoul", + "params": { + "ignoreMe": "true" + }, + "block": { + "id": "op", + "name": "블록 이름" + }, + "utterance": "발화 내용", + "lang": null, + "user": { + "id": "138", + "type": "accountId", + "properties": {} + } + }, + "bot": { + "id": "5fe45a6", + "name": "봇 이름" + }, + "action": { + "name": "yl", + "clientExtra": null, + "params": {}, + "id": "xx89p2dlfm", + "detailParams": {} + } +}</code></pre> +<ol start="2"> +<li>메인 함수</li> +</ol> +<pre><code class="bash">cargo add serde_json</code></pre> +<pre><code class="rust">use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder}; +use kakao_rs::prelude::*; +use serde_json::Value; + +#[get("/")] +async fn hello() -> impl Responder { + HttpResponse::Ok().body("Hello world!") +} + +#[post("/kakao")] +async fn kakao(kakao: web::Json<Value>) -> impl Responder { + let mut result = Template::new(); + + result.add_output(SimpleText::new("안녕하세요~").build()); + + let body = serde_json::to_string(&result).unwrap(); + + HttpResponse::Ok() + .content_type("application/json") + .body(body) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + HttpServer::new(|| { + App::new() + .service(hello) + .service(kakao) + }) + .bind(("127.0.0.1", 8080))? + .run() + .await +}</code></pre> +<p>해당 코드는 Rust의 Actix 웹 프레임워크에서 사용하는 함수 정의의 한 예입니다.</p> +<p>이 함수는 HTTP POST 요청을 처리하고 있으며, <code>/kakao</code> 라는 경로에 대한 요청을 처리하는 역할을 합니다.</p> +<p>함수의 정의에 대해 세부적으로 살펴보겠습니다.</p> +<h2 id="">함수 시그니처</h2> +<pre><code class="rust">#[post("/kakao")] +async fn kakao(kakao: web::Json<Value>) -> impl Responder { + let mut result = Template::new(); + + result.add_output(SimpleText::new("안녕하세요~").build()); + + let body = serde_json::to_string(&result).unwrap(); + + HttpResponse::Ok() + .content_type("application/json") + .body(body) +}</code></pre> +<p>위 함수는 다음과 같은 요소들로 구성되어 있습니다.</p> +<ul> +<li><code>async fn</code>: 함수가 비동기 함수임을 나타내는 키워드입니다. Rust에서는 이 키워드를 사용하여 비동기 함수를 정의하고, 이 함수 내에서는 <code>.await</code> 연산자를 사용하여 비동기 작업을 기다릴 수 있습니다.</li> +<li><code>kakao</code>: 함수의 이름입니다. 이 이름은 이 함수가 무슨 작업을 하는지를 설명하는 데 사용됩니다.</li> +<li><code>(kakao: web::Json<Value>)</code>: 함수의 인자 목록입니다. 이 함수는 <code>web::Json<Value></code> 타입의 인자 하나를 받습니다. 이 인자는 HTTP 요청의 본문을 JSON 형식으로 파싱한 결과입니다. <code>Value</code>는 <code>serde_json</code> 라이브러리에서 제공하는 타입으로, 임의의 JSON 데이터를 나타냅니다.</li> +<li><code>-> impl Responder</code>: 함수의 리턴 타입입니다. 이 함수는 <code>impl Responder</code>라는 리턴 타입을 가집니다.<code>Responder</code>는 Actix 웹 프레임워크에서 정의한 트레잇(trait)으로, HTTP 응답을 생성하는 메소드를 제공합니다.</li> +</ul> +<p>Template 부분은 kakao-rs 라이브러리를 참고하시면 모든 카카오톡 챗봇 응답 타입들을 일단 Template에 담아야 합니다.</p> +<p>1가지 이상의 응답을 보낼 수 있기 때문입니다. (ex. SimpleText, BasicCard 같이)</p> +<p>그후 <code>serde_json</code>의 <code>to_string</code>을 이용해서 serialize 가능한 구조체들을 쉽게 json string으로 변환해서 return 할 수 있습니다.</p> + <div class="bottomnav"> + <span class="back"><a href="52_ko.html" rel="prev">❮ 이전</a></span> + <span class="next"><a href="54_ko.html" rel="next">다음 ❯</a></span> + </div> + </div> + <div class="code"><center><img src="/ferris_lofi.png" alt="Rust Tutorial" width="80%" height="100%"></center></div> + </div> + <script> + var pres = document.querySelectorAll("pre>code"); + for (var i = 0; i < pres.length; i++) { + hljs.highlightElement(pres[i]); + } + var options = { + loadDelay: 0, + copyIconClass: "far fa-clipboard", + checkIconClass: "fa fa-check text-success", + blogURL: "http://rust-study.ajousw.kr/" + }; + window.highlightJsBadge(options); + </script> + + <footer> + <p><a target="_blank" rel="noopener" href="https://www.youtube.com/c/SoftwareToolTime">아주대학교 Software Tool Time</a> - Rust 튜토리얼 (Basic)</p> + </footer> + </body> +</html> \ No newline at end of file diff --git a/docs/54_ko.html b/docs/54_ko.html new file mode 100644 index 0000000..f42282d --- /dev/null +++ b/docs/54_ko.html @@ -0,0 +1,66 @@ +<!DOCTYPE html> + <html lang="ko"> + <head> + <title>Rust 튜토리얼 - 자기주도프로젝트</title> + + <meta charset="UTF-8"> + <meta content="text/html;charset=utf-8" http-equiv="Content-Type"> + <meta content="utf-8" http-equiv="encoding"> + <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2"> + <meta name="keywords" content="Rust, Programming, Learning"> + <meta name="description" content="Rust tutorial website based on tour_of_rust by 최석원"> + <meta name="theme-color" content="#ff6801"/> + <meta http-equiv="Cache-Control" content="max-age=3600"> + + <link rel="stylesheet" href="tour.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/night-owl.min.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.1/css/all.min.css"> + + <link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> + <link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> + <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> + <link rel="/manifest" href="./site.webmanifest"> + + <script src="//unpkg.com/@highlightjs/cdn-assets@11.7.0/highlight.min.js"></script> + + <script src="./tour.js" defer></script> + <!-- <script>hljs.highlightAll();</script> --> + <script src="./highlight.badge.min.js"></script> + </head> + <body> + <div class="tour"> + <div class="header"> + <span class="title"><a href="index.html">Rust 튜토리얼</a></span> + <span class="nav"> + <span class="toc"><a href="TOC_ko.html">목차</a></span> + </div> + <div class="page"> + <h1>Chapter 6 - Conclusion</h1> + <p>지금까지 <code>actix-rs</code>를 이용해서 카카오톡 챗봇 서버를 만들어보았습니다.</p> +<p>데이터베이스 연동이나 24시간 실행하는 등 방법들을 배워보고 추가해보시길 바랍니다. (ex. <a href="https://choiseokwon.tistory.com/332" target="_blank" rel="noopener">actix-rs + mongodb</a>)</p> + <div class="bottomnav"> + <span class="back"><a href="53_ko.html" rel="prev">❮ 이전</a></span> + <span class="next"><a href="chapter_7_ko.html" rel="next">다음 ❯</a></span> + </div> + </div> + <div class="code"><center><img src="/weather.png" alt="Rust Tutorial" width="80%" height="100%"></center></div> + </div> + <script> + var pres = document.querySelectorAll("pre>code"); + for (var i = 0; i < pres.length; i++) { + hljs.highlightElement(pres[i]); + } + var options = { + loadDelay: 0, + copyIconClass: "far fa-clipboard", + checkIconClass: "fa fa-check text-success", + blogURL: "http://rust-study.ajousw.kr/" + }; + window.highlightJsBadge(options); + </script> + + <footer> + <p><a target="_blank" rel="noopener" href="https://www.youtube.com/c/SoftwareToolTime">아주대학교 Software Tool Time</a> - Rust 튜토리얼 (Basic)</p> + </footer> + </body> +</html> \ No newline at end of file diff --git a/docs/8080.png b/docs/8080.png new file mode 100644 index 0000000000000000000000000000000000000000..c1f6a72e19706eebfaaf45187a5a47071d3a0759 GIT binary patch literal 6981 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 literal 0 HcmV?d00001 diff --git a/docs/TOC_ko.html b/docs/TOC_ko.html index 6ffd901..519b590 100644 --- a/docs/TOC_ko.html +++ b/docs/TOC_ko.html @@ -87,7 +87,11 @@ <li><a href="47_ko.html">Chapter 5 - Conclusion</a></li> </ul><h3><a href="chapter_6_ko.html">Chapter 6 - 웹 서버</a></h3><ul> <li><a href="49_ko.html">프로젝트 준비</a></li> -<li><a href="50_ko.html">Chapter 6 - Conclusion</a></li> +<li><a href="50_ko.html">Hello World API</a></li> +<li><a href="51_ko.html">main() && impl Responder</a></li> +<li><a href="52_ko.html">kakao-rs</a></li> +<li><a href="53_ko.html">실행</a></li> +<li><a href="54_ko.html">Chapter 6 - Conclusion</a></li> </ul><h3><a href="chapter_7_ko.html">마치며</a></h3><ul> </ul> </div> diff --git a/docs/chapter_7_ko.html b/docs/chapter_7_ko.html index 2c8989b..126cff6 100644 --- a/docs/chapter_7_ko.html +++ b/docs/chapter_7_ko.html @@ -36,9 +36,22 @@ </div> <div class="page"> <h1>마치며</h1> - <p>…</p> + <p><strong>Rust 언어 🎉</strong></p> +<p>여러분이 이 Rust 언어 영상 강좌를 끝까지 수강하신 것을 축하드립니다!</p> +<p>이 강좌에서는 Rust 언어의 기본부터 고급 문법, 소유권과 빌림, 콜렉션과 이터레이터, Cargo를 다루는 방법, 그리고 actix-rs를 이용한 카카오톡 챗봇 서버 제작에 이르기까지 다양한 주제를 다루었습니다.</p> +<p>우리는 챕터1에서 Rust의 소개와 설치 방법을 배웠고, 챕터2에서는 Rust의 기본 문법을 익혔습니다.</p> +<p>챕터3에서는 Rust의 핵심 개념인 소유권과 빌림에 대해 배웠고, 챕터4에서는 다양한 콜렉션과 이터레이터를 사용하는 방법을 다뤘습니다. </p> +<p>챕터5에서는 Rust의 고급 문법인 unsafe와 macro 등을 배웠고, 챕터6에서는 Rust의 패키지 관리자인 Cargo를 이용하는 방법을 익혔습니다.</p> +<p>마지막으로, 챕터7에서는 actix-rs 라이브러리를 이용하여 카카오톡 챗봇 서버를 제작하는 방법에 대해 배웠습니다.</p> +<p>이러한 지식들을 통해 여러분은 이제 Rust로 강력하고 효율적인 애플리케이션을 작성할 수 있는 기본적인 역량을 갖추게 되었습니다.</p> +<p>Rust 학습 여정이 이 강좌에서 끝나지 않길 바랍니다. 이제 Rust를 사용하여 여러분만의 프로젝트를 시작해보시기를 권장드립니다.</p> +<p>향후 프로젝트에서 만나는 도전과 문제를 해결하며, Rust에 대한 더 깊은 이해와 능력을 쌓아가시기 바랍니다.</p> +<p>이 강좌가 여러분의 Rust 학습에 도움이 되었기를 바라며, 여러분의 코딩 여정이 계속되기를 기원합니다. 감사합니다.</p> +<ul> +<li>최석원 (ikr@kakao.com)</li> +</ul> <div class="bottomnav"> - <span class="back"><a href="50_ko.html" rel="prev">❮ 이전</a></span> + <span class="back"><a href="54_ko.html" rel="prev">❮ 이전</a></span> </div> </div> diff --git a/docs/weather.png b/docs/weather.png new file mode 100644 index 0000000000000000000000000000000000000000..6be06f1e015021e3d58e03e5e0a535717ee610ff GIT binary patch literal 22243 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 S?;HaI1B0ilpUXO@geCw#Gx6X6 literal 0 HcmV?d00001 diff --git a/frontend/generate.js b/frontend/generate.js index 28bdf01..1232bc5 100644 --- a/frontend/generate.js +++ b/frontend/generate.js @@ -9,7 +9,16 @@ const rustExtension = { type: "lang", regex: /%rust%([^]+?)%end%/gi, replace: (s, match) => - `<pre><code class="rust">${match.trim().replace("<", "<")}</code></pre>`, + `<pre><code class="rust">${match + .trim() + .split("{{") + .join("<<") + .split("}}") + .join(">>") + .split("<") + .join("<") + .split(">") + .join(">")}</code></pre>`, }; const langExtension = { diff --git a/frontend/lessons/ko/chapter_3.yaml b/frontend/lessons/ko/chapter_3.yaml index fed7072..ad9e5ec 100644 --- a/frontend/lessons/ko/chapter_3.yaml +++ b/frontend/lessons/ko/chapter_3.yaml @@ -313,7 +313,7 @@ let scores = vec![("Alice", 50), ("Bob", 60)] .into_iter() .map(|(k, v)| (k.to_string(), v)) - .collect::<HashMap<String, u32>>(); + .collect::<HashMap<String, u32}}(); %end% diff --git a/frontend/lessons/ko/chapter_6.yaml b/frontend/lessons/ko/chapter_6.yaml index e1f2189..331bf44 100644 --- a/frontend/lessons/ko/chapter_6.yaml +++ b/frontend/lessons/ko/chapter_6.yaml @@ -32,11 +32,373 @@ # 준비물: - 1. public에서 접속 가능한 주소 (포트포워딩이나 AWS EC2를 이용) + 1. public에서 접속 가능한 머신 (포트포워딩이나 AWS EC2, [fly.io](https://fly.io) 등 이용) 2. 카카오 i 챗봇 만들기 [@링크](https://i.kakao.com/) 3. 카카오톡 채널 만들기 [@링크](https://center-pf.kakao.com/profiles) -- title: Chapter 6 - Conclusion +- title: Hello World API + source: >- + /8080.png + content_markdown: | + 이번 튜토리얼에서는 Rust와 Actix-web을 이용하여 'Hello World' 메시지를 출력하는 기본적인 웹 서버를 만들어 보겠습니다. + + ## 시작하기 + + 첫 단계로, 새로운 binary 기반의 Cargo 프로젝트를 생성합니다: + + ```bash + cargo new hello-world + cd hello-world + ``` + + 그 후, 프로젝트에 actix-web을 의존성으로 추가해야 합니다. + + 이를 위해 `Cargo.toml` 파일을 열고 다음과 같이 입력합니다: + + ```toml + [dependencies] + actix-web = "4" + ``` + + ## 핸들러 작성하기 + + 웹 서버에서 요청을 처리하기 위해 핸들러 함수를 작성합니다. + + 이 함수들은 비동기 함수로, 필요한 매개변수를 받아 HttpResponse를 반환합니다. + + 이 HttpResponse는 웹 서버가 클라이언트에게 보낼 응답입니다. + + `src/main.rs` 파일을 다음과 같이 수정합니다: + + ```rust + use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder}; + + #[get("/")] + async fn hello() -> impl Responder { + HttpResponse::Ok().body("Hello world!") + } + + #[post("/echo")] + async fn echo(req_body: String) -> impl Responder { + HttpResponse::Ok().body(req_body) + } + + async fn manual_hello() -> impl Responder { + HttpResponse::Ok().body("Hey there!") + } + ``` + + 각 핸들러는 HTTP 메소드와 경로에 따라 요청을 처리합니다. + + 수동으로 경로를 설정하고 싶다면, 그에 맞는 함수를 작성하면 됩니다. + + ## App 생성 및 요청 핸들러 등록 + + 다음 단계로, App 인스턴스를 생성하고 요청 핸들러를 등록합니다. + + 경로 정보가 있는 핸들러는 `App::service`를 사용하고, 수동으로 경로를 설정한 핸들러는 `App::route`를 사용합니다. + + ```rust + #[actix_web::main] + async fn main() -> std::io::Result<()> { + HttpServer::new(|| { + App::new() + .service(hello) + .service(echo) + .route("/hey", web::get().to(manual_hello)) + }) + .bind(("127.0.0.1", 8080))? + .run() + .await + } + ``` + + ## 서버 실행 + + 이제 `cargo run` 명령어를 통해 프로그램을 컴파일하고 실행할 수 있습니다. + + 웹 브라우저에서 http://127.0.0.1:8080/ 주소로 접속하면 'Hello World' 메시지를 확인할 수 있습니다. + + 이 간단한 예제를 통해 Rust와 Actix-web을 이용하여 웹 서버를 어떻게 만드는지 배웠습니다. + + 이러한 기본 원리를 이용하면 다양한 웹 서비스를 만들어볼 수 있습니다. + +- title: main() && impl Responder + source: >- + /8080.png + content_markdown: | + ## actix-rs main() + + 아래는 웹 서버를 시작하는 역할을 합니다. + + ```rust + #[actix_web::main] + async fn main() -> std::io::Result<()> { + HttpServer::new(|| { + App::new() + .service(hello) + .service(echo) + .route("/hey", web::get().to(manual_hello)) + }) + .bind(("127.0.0.1", 8080))? // localhost:8080 or 127.0.0.1:8080 + .run() + .await + } + ``` + + 여기서 `#[actix_web::main]`은 Actix 웹 프레임워크에서 제공하는 매크로로, 이를 이용해 비동기 메인 함수를 생성할 수 있습니다. + + 그리고 `HttpServer::new` 함수를 호출하여 새로운 HTTP 서버 인스턴스를 만듭니다. + + 이 함수의 인자로는 클로저가 들어가며, 이 클로저 내부에서 App 인스턴스를 만들고, + + 우리가 앞서 정의한 hello, echo, 그리고 manual_hello 함수를 각각 서비스로 등록합니다. + + 또한, `/hey` 라는 경로는 수동으로 설정된 경로입니다. + + `web::get().to(manual_hello)`를 통해 get 요청이 들어왔을 때 `manual_hello` 함수가 호출되도록 설정했습니다. + + 그 후 `.bind(("127.0.0.1", 8080))?`를 통해 서버를 로컬 호스트의 8080 포트에 바인딩하게 됩니다. + + 만약 바인딩에 실패하면 에러를 반환하며 프로그램은 종료됩니다. + + 마지막으로 `.run().await`를 통해 서버를 실행시키며, 이 서버는 비동기적으로 작동하게 됩니다. + + ## impl Responder + + Rust의 'Trait'에 대해 알아보고, 특히 actix-web에서 제공하는 'Responder' Trait에 대해 살펴보겠습니다. + + Responder는 웹 응답을 만드는데 굉장히 중요한 역할을 하는 Trait입니다. + + ## Trait 이란? + + Rust에서 Trait는 특정 기능이나 행동을 정의한 것으로, + + Trait를 구현하면 해당 구조체나 열거형은 Trait에서 정의된 메소드를 사용할 수 있게 됩니다. + + Rust는 상속 대신 Trait를 사용하여 코드의 재사용성과 모듈성을 증가시킵니다. + + ## Responder Trait + + Responder는 actix-web에서 제공하는 Trait 중 하나로, HTTP 응답을 생성하는 메소드를 제공합니다. + + 이 Trait를 이용하면 웹 서버가 클라이언트에게 보내는 HTTP 응답을 쉽게 만들 수 있습니다. + + Responder Trait은 두 가지 메소드를 정의합니다: + + 1. `respond_to`: HttpRequest 객체를 받아 HttpResponse를 생성하는 메소드로, 이는 핸들러에서 클라이언트의 요청을 받아 적절한 응답을 생성하는 데 사용됩니다. + 2. `customize`: 응답을 커스터마이징 할 수 있는 메소드로, 이 메소드는 Responder가 구현되어 있는 경우 사용할 수 있습니다. + + ## Responder 사용하기 + + 핸들러 함수에서는 `impl Responder`를 리턴 타입으로 사용합니다. + + 이렇게 하면 어떤 값이든, 그 값이 Responder Trait를 구현하고 있다면 리턴할 수 있게 됩니다. + + 예를 들어, 'String', 'HttpResponse' 등은 모두 Responder를 구현하고 있습니다. + + 이를 통해 해당 값을 리턴하는 핸들러를 쉽게 만들 수 있습니다. + + Rust의 강력한 타입 시스템 덕분에 컴파일 타임에 각 핸들러가 어떤 타입의 값을 리턴하는지를 확인할 수 있게 되어, + + 런타임 에러를 사전에 방지하는 데 매우 유용합니다. + + 결국, Responder는 웹 응답을 만드는 과정을 추상화하고, 다양한 타입의 값을 HTTP 응답으로 쉽게 변환할 수 있게 해주는 역할을 합니다. + + 이를 통해 강력하면서도 유연한 웹 응답을 만들 수 있게 됩니다. + + Responder를 활용하면 웹 서버 개발이 훨씬 편리해집니다. +- title: kakao-rs + source: >- + /ferris_lofi.png + content_markdown: | + 카카오톡 API 템플릿을 쉽게 만들어 주는 `kakao-rs` 라이브러리에 대해 알아보겠습니다. + + ## kakao-rs 라이브러리란? + + kakao-rs는 Rust 언어로 작성된 카카오 챗봇 서버를 만들 때 사용할 수 있는 라이브러리입니다. + + 이 라이브러리는 SimpleText, SimpleImage, ListCard, Carousel, BasicCard, CommerceCard, ItemCard 등의 JSON 데이터를 쉽게 생성할 수 있도록 돕는 도구들을 제공합니다. + + ## 사용 방법 + + kakao-rs 라이브러리를 사용하려면, 먼저 프로젝트의 `Cargo.toml` 파일에 kakao-rs를 의존성으로 추가해야 합니다. + + ```toml + [dependencies] + kakao-rs = "0.3" + ``` + + 이 라이브러리를 이용하면 다양한 종류의 버튼(예: 공유 버튼, 링크 버튼, 일반 메시지 버튼, 전화 버튼 등)을 쉽게 만들 수 있습니다. + + ## 카카오 JSON 데이터 연동 + + kakao-rs는 카카오 JSON 데이터와의 연동이 매우 간단합니다. + + 유저의 발화문을 얻기 위해서는 아래와 같이 작성하면 됩니다. + + ```rust + #[post("/end")] + pub async fn test(kakao: web::Json<Value>) -> impl Responder { // actix + println!("{}", kakao["userRequest"]["utterance"].as_str().unwrap()); // 발화문 + unimplemented!() + } + ``` + + 이 라이브러리를 이용하면 다양한 형태의 카카오 챗봇 메시지를 쉽게 생성할 수 있습니다. + + 예를 들어, ListCard를 생성하는 코드는 아래와 같습니다. + + ```rust + let mut list_card = ListCard::new("리스트 카드 제목!"); // 제목 + // ... + result.add_output(list_card.build()); // moved list_card's ownership + ``` + + kakao-rs 라이브러리를 통해 SimpleText, SimpleImage, BasicCard, CommerceCard, Carousel 등의 + + 다양한 형태의 카카오 챗봇 메시지를 쉽게 생성할 수 있습니다. + + 카카오 챗봇 서버를 Rust로 구현하려는 개발자들에게 kakao-rs 라이브러리는 매우 유용한 도구가 될 것입니다. +- title: 실행 source: >- /ferris_lofi.png content_markdown: | - CSW + `kakao-rs`를 `actix-rs`에 적용시켜보겠습니다. + + 단순하게 SimpleText를 반환할 것이며 더 많은 카카오톡 반응 디자인은 다음을 참고하세요: [@링크](https://i.kakao.com/docs/skill-response-format) + + <div align="center"> + <p> + <img src="https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2Fn5Ud0%2FbtraVUpTmyM%2FLSdvhi5uKkzx9tcN2SFbh1%2Fimg.png"> + </p> + </div> + + + 1. 의존성 추가 + + ```bash + cargo add kakao-rs + ``` + + 카카오톡 챗봇 POST json 구조: 챗봇 관리자 > 스킬 > 편집 + + ```json + "intent": { + "id": "hequ", + "name": "블록 이름" + }, + "userRequest": { + "timezone": "Asia/Seoul", + "params": { + "ignoreMe": "true" + }, + "block": { + "id": "op", + "name": "블록 이름" + }, + "utterance": "발화 내용", + "lang": null, + "user": { + "id": "138", + "type": "accountId", + "properties": {} + } + }, + "bot": { + "id": "5fe45a6", + "name": "봇 이름" + }, + "action": { + "name": "yl", + "clientExtra": null, + "params": {}, + "id": "xx89p2dlfm", + "detailParams": {} + } + } + ``` + + 2. 메인 함수 + + ```bash + cargo add serde_json + ``` + + ```rust + use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder}; + use kakao_rs::prelude::*; + use serde_json::Value; + + #[get("/")] + async fn hello() -> impl Responder { + HttpResponse::Ok().body("Hello world!") + } + + #[post("/kakao")] + async fn kakao(kakao: web::Json<Value>) -> impl Responder { + let mut result = Template::new(); + + result.add_output(SimpleText::new("안녕하세요~").build()); + + let body = serde_json::to_string(&result).unwrap(); + + HttpResponse::Ok() + .content_type("application/json") + .body(body) + } + + #[actix_web::main] + async fn main() -> std::io::Result<()> { + HttpServer::new(|| { + App::new() + .service(hello) + .service(kakao) + }) + .bind(("127.0.0.1", 8080))? + .run() + .await + } + ``` + + 해당 코드는 Rust의 Actix 웹 프레임워크에서 사용하는 함수 정의의 한 예입니다. + + 이 함수는 HTTP POST 요청을 처리하고 있으며, `/kakao` 라는 경로에 대한 요청을 처리하는 역할을 합니다. + + 함수의 정의에 대해 세부적으로 살펴보겠습니다. + + ## 함수 시그니처 + + ```rust + #[post("/kakao")] + async fn kakao(kakao: web::Json<Value>) -> impl Responder { + let mut result = Template::new(); + + result.add_output(SimpleText::new("안녕하세요~").build()); + + let body = serde_json::to_string(&result).unwrap(); + + HttpResponse::Ok() + .content_type("application/json") + .body(body) + } + ``` + + 위 함수는 다음과 같은 요소들로 구성되어 있습니다. + + - `async fn`: 함수가 비동기 함수임을 나타내는 키워드입니다. Rust에서는 이 키워드를 사용하여 비동기 함수를 정의하고, 이 함수 내에서는 `.await` 연산자를 사용하여 비동기 작업을 기다릴 수 있습니다. + - `kakao`: 함수의 이름입니다. 이 이름은 이 함수가 무슨 작업을 하는지를 설명하는 데 사용됩니다. + - `(kakao: web::Json<Value>)`: 함수의 인자 목록입니다. 이 함수는 `web::Json<Value>` 타입의 인자 하나를 받습니다. 이 인자는 HTTP 요청의 본문을 JSON 형식으로 파싱한 결과입니다. `Value`는 `serde_json` 라이브러리에서 제공하는 타입으로, 임의의 JSON 데이터를 나타냅니다. + - `-> impl Responder`: 함수의 리턴 타입입니다. 이 함수는 `impl Responder`라는 리턴 타입을 가집니다.`Responder`는 Actix 웹 프레임워크에서 정의한 트레잇(trait)으로, HTTP 응답을 생성하는 메소드를 제공합니다. + + Template 부분은 kakao-rs 라이브러리를 참고하시면 모든 카카오톡 챗봇 응답 타입들을 일단 Template에 담아야 합니다. + + 1가지 이상의 응답을 보낼 수 있기 때문입니다. (ex. SimpleText, BasicCard 같이) + + 그후 `serde_json`의 `to_string`을 이용해서 serialize 가능한 구조체들을 쉽게 json string으로 변환해서 return 할 수 있습니다. +- title: Chapter 6 - Conclusion + source: >- + /weather.png + content_markdown: | + 지금까지 `actix-rs`를 이용해서 카카오톡 챗봇 서버를 만들어보았습니다. + + 데이터베이스 연동이나 24시간 실행하는 등 방법들을 배워보고 추가해보시길 바랍니다. (ex. [actix-rs + mongodb](https://choiseokwon.tistory.com/332)) diff --git a/frontend/lessons/ko/chapter_7.yaml b/frontend/lessons/ko/chapter_7.yaml index 81dcb40..b5b977b 100644 --- a/frontend/lessons/ko/chapter_7.yaml +++ b/frontend/lessons/ko/chapter_7.yaml @@ -2,4 +2,26 @@ source: >- /ferris_lofi.png content_markdown: | - ... + **Rust 언어 🎉** + + 여러분이 이 Rust 언어 영상 강좌를 끝까지 수강하신 것을 축하드립니다! + + 이 강좌에서는 Rust 언어의 기본부터 고급 문법, 소유권과 빌림, 콜렉션과 이터레이터, Cargo를 다루는 방법, 그리고 actix-rs를 이용한 카카오톡 챗봇 서버 제작에 이르기까지 다양한 주제를 다루었습니다. + + 우리는 챕터1에서 Rust의 소개와 설치 방법을 배웠고, 챕터2에서는 Rust의 기본 문법을 익혔습니다. + + 챕터3에서는 Rust의 핵심 개념인 소유권과 빌림에 대해 배웠고, 챕터4에서는 다양한 콜렉션과 이터레이터를 사용하는 방법을 다뤘습니다. + + 챕터5에서는 Rust의 고급 문법인 unsafe와 macro 등을 배웠고, 챕터6에서는 Rust의 패키지 관리자인 Cargo를 이용하는 방법을 익혔습니다. + + 마지막으로, 챕터7에서는 actix-rs 라이브러리를 이용하여 카카오톡 챗봇 서버를 제작하는 방법에 대해 배웠습니다. + + 이러한 지식들을 통해 여러분은 이제 Rust로 강력하고 효율적인 애플리케이션을 작성할 수 있는 기본적인 역량을 갖추게 되었습니다. + + Rust 학습 여정이 이 강좌에서 끝나지 않길 바랍니다. 이제 Rust를 사용하여 여러분만의 프로젝트를 시작해보시기를 권장드립니다. + + 향후 프로젝트에서 만나는 도전과 문제를 해결하며, Rust에 대한 더 깊은 이해와 능력을 쌓아가시기 바랍니다. + + 이 강좌가 여러분의 Rust 학습에 도움이 되었기를 바라며, 여러분의 코딩 여정이 계속되기를 기원합니다. 감사합니다. + + - 최석원 (ikr@kakao.com) diff --git a/package-lock.json b/package-lock.json index c6f33e2..50e6e87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,9 +37,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.0.tgz", - "integrity": "sha512-vITaYzIcNmjn5tF5uxcZ/ft7/RXGrMUIS9HalWckEOF6ESiwXKoMzAQf2UW0aVd6rnOeExTJVd5hmWXucBKGXQ==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" @@ -69,9 +69,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.38.0.tgz", - "integrity": "sha512-IoD2MfUnOV58ghIHCiil01PcohxjbYR/qCxsoC+xNgUwh1EY8jOOrYmu3d3a71+tJJ23uscEV4X2HJWMsPJu4g==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.39.0.tgz", + "integrity": "sha512-kf9RB0Fg7NZfap83B3QOqOGg9QmD9yBudqQXzzOtn3i4y7ZUXe5ONeW34Gwi+TxhH4mvj72R1Zc300KUMa9Bng==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -805,15 +805,15 @@ } }, "node_modules/eslint": { - "version": "8.38.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.38.0.tgz", - "integrity": "sha512-pIdsD2jwlUGf/U38Jv97t8lq6HpaU/G9NKbYmpWpZGw3LdTNhZLbJePqxOXGB5+JEKfOPU/XLxYxFh03nr1KTg==", + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.39.0.tgz", + "integrity": "sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", "@eslint/eslintrc": "^2.0.2", - "@eslint/js": "8.38.0", + "@eslint/js": "8.39.0", "@humanwhocodes/config-array": "^0.11.8", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", @@ -823,7 +823,7 @@ "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", + "eslint-scope": "^7.2.0", "eslint-visitor-keys": "^3.4.0", "espree": "^9.5.1", "esquery": "^1.4.2", @@ -901,9 +901,9 @@ } }, "node_modules/eslint-module-utils": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.4.tgz", - "integrity": "sha512-j4GT+rqzCoRKHwURX7pddtIPGySnX9Si/cgMI5ztrcqOPtk5dDEeZ34CQVPphnqkJytlc97Vuk05Um2mJ3gEQA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", "dev": true, "dependencies": { "debug": "^3.2.7" @@ -977,9 +977,9 @@ } }, "node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.0.tgz", + "integrity": "sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", @@ -987,6 +987,9 @@ }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { @@ -1860,9 +1863,9 @@ "dev": true }, "node_modules/lru-cache": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.0.1.tgz", - "integrity": "sha512-C8QsKIN1UIXeOs3iWmiZ1lQY+EnKDojWd37fXy1aSbJvH4iSma1uy2OWuoB3m4SYRli5+CUjDv3Dij5DVoetmg==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.1.1.tgz", + "integrity": "sha512-65/Jky17UwSb0BuB9V+MyDpsOtXKmYwzhyl+cOa9XUiI4uV2Ouy/2voFP3+al0BjZbJgMBD8FojMpAf+Z+qn4A==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -2170,9 +2173,9 @@ "dev": true }, "node_modules/path-scurry": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.6.4.tgz", - "integrity": "sha512-Qp/9IHkdNiXJ3/Kon++At2nVpnhRiPq/aSvQN+H3U1WZbvNRK0RIQK/o4HMqPoXjpuGJUEWpHSs6Mnjxqh3TQg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.7.0.tgz", + "integrity": "sha512-UkZUeDjczjYRE495+9thsgcVgsaCPkaw80slmfVFgllxY+IO8ubTsOpFVjDPROBqJdHfVPUFRHPBV/WciOVfWg==", "dev": true, "dependencies": { "lru-cache": "^9.0.0", @@ -2216,9 +2219,9 @@ } }, "node_modules/prettier": { - "version": "2.8.7", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.7.tgz", - "integrity": "sha512-yPngTo3aXUUmyuTjeTUT75txrf+aMh9FiD7q9ZE/i6r0bPb22g4FsE6Y338PQX1bmfy08i9QQCB7/rcUAVntfw==", + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz", + "integrity": "sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==", "dev": true, "bin": { "prettier": "bin-prettier.js" @@ -2278,14 +2281,14 @@ } }, "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.0.tgz", + "integrity": "sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" + "define-properties": "^1.2.0", + "functions-have-names": "^1.2.3" }, "engines": { "node": ">= 0.4" diff --git a/package.json b/package.json index a31d855..bde6e3b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "rust", "private": true, - "version": "1.0.0", + "version": "1.0.5", "scripts": { "lint:lessons": "prettier --write ./frontend/lessons/*/*.yaml", "lint": "npm run lint:lessons && npm run lint:webassembly && eslint --fix generate.js docs/", @@ -9,8 +9,9 @@ "build": "node ./frontend/generate.js ./frontend/lessons docs", "serve": "npm run build && python fast.py", "clean": "rimraf --glob ./docs/*.html", - "watch": "nodemon -w ./frontend/lessons/**/* -e yaml --exec npm run serve", - "tauri": "tauri" + "watch": "nodemon -w ./frontend/lessons/**/* -w ./frontend/generate.js -w ./docs/tour.css -w ./docs/tour.js -e yaml --exec npm run serve", + "tauri": "tauri", + "ajou": "xcopy .\\docs\\* ..\\rust-study.ajousw.kr\\docs\\ /EXCLUDE:exclude.txt /E /Y >nul 2>&1 && xcopy .\\frontend\\* ..\\rust-study.ajousw.kr\\frontend\\ /EXCLUDE:exclude.txt /E /Y >nul 2>&1" }, "dependencies": { "js-yaml": "^4.1.0", -- GitLab