[flutter] daily record-whitebrew2
11.10
커피 한잔 먹고 시작.
hoyoul — 11/10/2023 8:41 AM Adapter adapter = JsonApiAdapter(‘host.example.com’, ‘/path/to/rest/api’); [8:45 AM] adapter를 사용하는 방법이라고 한다. domain주소를 쓰고 path를 기술하는데, path에는 resource가 uri로 기술되어 있다고 보면 된다. 이렇게 adapter로 연결하면 get,put,과 같은 요청을 할수 있을꺼라고 생각된다. [8:47 AM] rest api 사용방법을 readme에서 알려주는거 같다. [8:47 AM] rest_data package 사용법 [8:47 AM]
- adapter로 연결한다.
[8:48 AM] Adapter adapter = JsonApiAdapter(‘host.example.com’, ‘/path/to/rest/api’); [8:48 AM] 위에서 말한대로 adapter를 연결한다.
hoyoul — 11/10/2023 8:48 AM
- define models
[8:48 AM] For each of your REST API resources, you should define a model class extending JsonApiModel which provides the following getters:
Map<String, dynamic> get attributes see specs Map<String, dynamic> get relationships see specs Iterable<dynamic> get included see specs Iterable<dynamic> get errors see specs together with helper methods (e.g. idFor(), idsFor(), typeFor(), setHasOne() etc. - see the source for more details). [8:52 AM] model을 정의한다. 정의된 model을 instance로 생각하자. 그리고 json은 이 model instance 를 file로 만든 것이다. model을 network을 통해서 다른 컴퓨터로 보낼수가 없다. 직렬화해야 한다. string으로 직렬화하던, byte로 직렬화하던, 직렬화한게 json파일이다. xml도 될수 있고, binary file도 될 수 있다. 그런데json이 유명하고 잘 사용될 뿐이다. (edited) [8:54 AM] 여기선 서버와 클라이언트간에 주고받을 객체를 model로 만드는 것이다. 서버나 client나 동일한 class여야 한다. [8:54 AM] 그리고 만든 model을 json으로 보내면 json으로 받아서 바로 model객체로 생성하게 하기 위해서 model은 JsonAPIModel이란것을 사용한다.
hoyoul — 11/10/2023 8:59 AM 여기보면 JsonApiModel이란 class에서 제공하는 함수들이 보인다. get attributes, get relationships, get included, get errors 어디서 많이 본것들이다. 그렇다. json api 1.0에 기술된 top level 요소들이다. [8:59 AM] json api 1.0을 따른다는 것을 알수 있다.
hoyoul — 11/10/2023 9:07 AM attributes,relationships는 Map으로 return하고 included와 error는 iterable로 return한다. [9:09 AM] map도 자료구조고 iterable도 자료구조다. map은 key value쌍을 가진 자료구조고, iterable은 list를 생각하면 된다. iterable이라는 말그대로 반복에 사용될수 있는 구조를 뜻한다. 보통 array나 list 같은 자료구조에 적용된거라서, 그냥 list를 생각하면 편하다. [9:10 AM] json api 1.0문서에 보면 error는 error list로 구현하는게 must다. relationships와 included는 may이긴 하지만, 여튼 보내는 구조도 이미 api문서에 정의되어 있다. [9:11 AM] class Address extends JsonApiModel { // Constructors
Address(JsonApiDocument doc) : super(doc); Address.init(String type) : super.init(type);
// Attributes
String get street => getAttribute<String>(‘street’); set street(String value) => setAttribute<String>(‘street’, value);
String get city => getAttribute<String>(‘city’); set city(String value) => setAttribute<String>(‘city’, value);
String get zip => getAttribute<String>(‘zip’); set zip(String value) => setAttribute<String>(‘zip’, value);
// Has-One Relationships
String get countryId => idFor(‘country’); set country(Country model) => setHasOne(‘country’, model); }
class Country extends JsonApiModel { Country(JsonApiDocument doc) : super(doc); }
hoyoul — 11/10/2023 9:16 AM 예제가 나와있다. Address model인데, 생성자의 인수로 json document를 받는다. 좀 부가 설명을 하자면, 맨처음 adapter를 만들었다. adapter에서 요청을 하면 응답으로 json document를 받아올 것이다. 이것을 객체로 만들어야 한다. 어떻게 만들까? 위에처럼 JsonApiModel을 상속한 model을 생성할때, 생성자에 adapter로 받은 json을 넣어주거나, 아니면 빈 json document를 넣어주면, 아래 attribute, relationships, included같은 주석및의 함수들이 호출되서 json document를 만들어줄 꺼 같다.
hoyoul — 11/10/2023 9:24 AM json파일로 부터 아니면 server로 부터 street,city zip을 받아서 객체를 만든다. 그런데 보면 relathionship에 연관 객체로 country가 있다. 이것은 country라는 model이 address와 hasone relationship을 있다는 정보만 알려준다. country객체도 만들수 있다. oop나 relational db에서 사용하는 용어다. has one은 1:1관계를 얘기한다. 즉, 예를 들면, people이란 class를 만들었고, name이라는 class를 만들었다고 하자. hoyoul이란 instance에는 name이라는 한개의 instance만 매핑된다. 이런 관계를 1:1 mapping이라고 하고 has one은 보통 people이라는 class에 name이라는 member가 들어가 있다. 그리고 들어간 name이 모습때문에 has라는 단어가 들어간것이다. [9:27 AM] 위의 코드를 일반적인 class로 만들면 다음과 같을 것이다. class Address{ Address(); //생성자. string street; string city; string zip; Country model; } class Country{ Country(); //생성자 } [9:28 AM] 그런데 여기에는 included가 빠져있다. 그리고 country에 대한 정보만 제공한다. member에 대한 처리는 하지 않은것이다. [9:29 AM] json api 1.0에 나와 있듯이, relationships object의 멤버변수는 included로 보낼 수 있고, uri로 알려줄수도 있다. 근데 uri로 알려주는 부분도 없고, included도 없다. 그런데 이것은 다 option이라서 may로 기술되어 있기 때문에 legit하다.
hoyoul — 11/10/2023 9:31 AM 다음을 보자. [9:32 AM] Reading
Invoking REST APIs is as simple as calling async methods on your adapter object. Such methods return one or more JsonApiDocument objects, which can be used to build your model objects.
Finding a specific record
var address = Address(await adapter.find(‘addresses’, ‘1’)); Will send the request: GET /addresses/1. [9:34 AM] Address는 위에서 봤듯이 JsonApiModel을 상속받고 생성자로 jsonApiDocument를 받는다. adapter로 addresses collection의 1번 record를 요구한다. 그러면 json문서를 받아서 jsonApiDocument로 변환해서 address객체를 만드는 것이다. 당연한 거라서 pass. [9:35 AM] Finding all records
Iterable<Country> countries = adapter.findAll(‘countries’) .map<Country>((jsonApiDoc) => Country(jsonApiDoc)); Will send the request: GET /countries. [9:37 AM] adapter의 find나 findAll이나 Get method요청이다. countries collection을 다 가져와서 countries 에 assign한다.
hoyoul — 11/10/2023 9:39 AM 구조는 chain rule인데, map()는 받아온 JsonApi문서를 Country type의 객체로 mapping해서 countries에 assign한다고 보면된다. Country타입은 이미 있어야 한다. JsonApiModel형태로… [9:39 AM] Finding N specific records
Iterable<Address> addresses = (await adapter.findMany(‘addresses’, [‘1’, ‘2’, ‘3’])) .map<Address>((jsonApiDoc) => Address(jsonApiDoc)); GET /addresses?filter[id]=1,2,3
hoyoul — 11/10/2023 9:47 AM 위에 예하고 비슷하다. get요청해서 1,2,3 레코드를 가져온다. 그대로 사용하면 된다. [9:47 AM] Querying
Iterable<Address> addresses = (await adapter.query(‘addresses’, {‘q’: ‘miami’})) .map<Address>((jsonApiDoc) => Address(jsonApiDoc)); GET /addresses?filter[q]=miami [9:52 AM] query는 adapter의 query함수를 사용한다. q라는 항목이 miami인 address객체를 요청하고 json으로 받아오는 것이다. [9:52 AM] Writing
Create
You’ll start with an empty model object, whose attributes and relationships will be set based on user input:
var address = Address.init(); address.street = ‘9674 Northwest 10th Avenue’; address.city = ‘Miami’; address.zip = ‘33150’; address.country = Country.peek(‘US’); // Assume all countries are cached, see “Caching” section later To persist your model on your REST API backend, just invoke the Adapter’s save() method, which will return a new Address object:
var savedAddress = Address(await adapter.save(endpoint, address.jsonApiDoc)); The above line will send the request: POST /addresses with the address model object serialized as a JSON:API Document.
hoyoul — 11/10/2023 9:54 AM Model을 세팅한다. 그리고 adapter.save()를 사용해서 json으로 변환후 해당 주소에 보낸다. 서버에 json을 보내는것이다. [9:56 AM] Update
Assume you have an existing model object, and you edit some attributes based on user input:
var address = Address(await adapter.find(‘addresses’, ‘1’)); address.street = ‘9674 Northwest 10th Avenue’; address.zip = ‘33150’; To persist your model on your REST API backend, just invoke the Adapter’s save() method, which will return a new Address object:
var savedAddress = Address(await adapter.save(endpoint, address.jsonApiDoc)); The above line will send the request: PUT /addresses with the address model object serialized as a JSON:API Document. [9:57 AM] 서버에 요청해서 1번 record에 해당하는 Address객체를 서버로 부터 가져온다. 수정한다. 그리고 다시 보내면 서버에서 update된다. address를 받아올때, id가 있기 때문에 update가 된다. [9:58 AM] Caching
JsonApiAdapter comes with a basic caching mechanism built-in: a simple Map in-memory. Models fetched from the backend are automatically cached on any read request, and the cached ones are returned on subsequent read requests for the same model id.
When you only want already cached data, you can use Adapter’s methods starting with the peek prefix.
Invalidation must be handled manually, passing forceReload = true to find* methods. [9:58 AM] 오…이게 좀 색다른거다. 나머지는 그냥 다른언어에도 사용되는 기술인데..caching은 안 사용해봤다. 뭔지 읽어보자.
hoyoul — 11/10/2023 10:02 AM 음 별거 아니다. 그냥 우리가 adapter를 사용해서 address객체를 find로 요청했다고 하자. 그러면 cache에 저장이 된다고 한다. 어떤 find를 날려도, 다 cache에 먼저 저장이 된다는 것이다. 이렇게 하면 장단점이 있다. 원래 cache는 속도향상이라는 장점이 있지만, inconsistent 문제가 항상 지적된다. 단점이다. 뭔소리냐면, 내가 adapter로 1번 address객체를 server로부터 가져왔다. (edited) [10:03 AM] 그런데 다음에 또 1번이 필요하다. 그러면 adapter.find(address,1)로 또 요청할텐데, 그러면 cache에서 가져온다는 얘기다. [10:04 AM] 만일 서버에서 1번 address의 객체에 수정이 발생했어도, 우리는 cache에서 가져온 address를 사용하는 문제가 생긴다. [10:06 AM] peek라고 붙은건 cache에서 가져온다고 보면 된다. 여기서 수정이 필요하다. 그냥 find로 가져오면 cache에 있는지 없는지 확인하고 없으면 server에서 가져와서 cache에 넣는것이고, peek는 무조건 cache에서 가져온다. 이게 더 정확하다. (edited) [10:07 AM] 여기서 inconsistency를 invalidation이라고 표현을 했는데, 똑같은 말이다. cache의 데이터와 서버의 데이터가 불일치하는걸 말한다. [10:08 AM] 이걸 고치는 방법은 forceReload=true로 하고 find* 를 사용하라고 한다. [10:08 AM] 이렇게 하면 서버에서 data를 가져와서 cache에 저장된 data와 일치 시켜서 이런 단점을 없애준다고 한다.
hoyoul — 11/10/2023 10:10 AM adapter.find*(forceReload=true, ) 이렇게 하지 않을까? 이 방법은 readme에 안 나와 있다. [10:10 AM] 여튼 필요할 때 사용하면 될듯하다. [10:10 AM] 10분간 휴식하자. 여기까지 rest_data를 사용하는 방법을 알았다면, kholdem 소스 분석을 다시하자. gamecontroller를 다시 보자. [10:21 AM] class GameController extends GetxController { static GameController get to => Get.find();
var user = User(JsonApiDocument(’’, ‘’, {}, {})).obs; var users = <User>[].obs;
var rooms = <GameRoom>[].obs; var currentRoom = GameRoom(JsonApiDocument(’’, ‘’, {}, {})).obs; var currentGame = Game(JsonApiDocument(’’, ‘’, {}, {})).obs; var currentPlayerId = “".obs; var winner = User(JsonApiDocument(’’, ‘’, {}, {})).obs; var pot = “".obs;
var isShowBet = false.obs; [10:22 AM] main에서 GameController를 생성하는데, 생성하면 뭘 하냐? [10:23 AM] User, GameRoom,Game이란 객체를 생성하는데, 모두 json 빈 문서를 넣어서 초기화 한다. 왜그럴까? 여기에 obs를 걸었기 때문이다. [10:24 AM] 즉 빈문서에 obs를 걸고, json을 받거나 하면, view에서 obx를 사용해서 update할 의도로 이렇게 한거다. [10:25 AM] 이부분이 명확하진 않다. 어떤식으로 왜 이렇게 했는지…좀 구름에 뜬 기분이랄까…
hoyoul — 11/10/2023 10:26 AM 하지만, 대충이해는 했다. 내가 설명하면, 아 그렇게 이해하는거 맞아요. 라고 할테지만, 와 닿지는 않는기분…잘 모르겠다. [10:26 AM] 다시 분석해 보자. [10:26 AM] users에도 obs가 걸렸는데, 이것은 users가 추가되면 알려줄려고 하는것이다. [10:26 AM] rooms도 마찬가지고 [10:27 AM] pot는 뭔진 모르겠지만, string type인거 같다. [10:27 AM] isShowBet도 boolean인거 같고… [10:28 AM] 그런데 static하게 type을 기술해도 될듯한데, 왜 var을 사용했는지는 모르겠다. [10:29 AM] 여튼 이부분은 다 초기화 과정이라고 보면 된다. user라는 객체를 만들고, users라는 list를 만들고, rooms라는 list를 만들고 …다 obs를 걸어서 변동이 생기면, 알려주는 구조다. 언제 변동이 생길까? 통신이 시작하면 변동이 생길것이다. 아직 통신은 시작하지 않은것이다. [10:30 AM] 통신이 시작하면 값이 변경되고 연관된 obx들은 호출 될 것이다. @override void onInit() async { // await initGame(); await KholdemSocket().initSocket(); super.onInit(); } [10:33 AM] onInit()는 getXcontroller에서 정의된 함수인데, 호출 시점이 궁금하다. 물론 GetXController를 상속받은 GameController가 생성되면 생성자 처리를 먼저할 것이다. 즉 위의 멤버변수들이 다 비어있는 상태로 생성되고, 그 이후에 onInit()가 호출될듯하다.
hoyoul — 11/10/2023 10:34 AM 공식 문서를 보자. GetxService
This class is like a GetxController, it shares the same lifecycle ( onInit(), onReady(), onClose()). But has no “logic” inside of it. It just notifies GetX Dependency Injection system, that this subclass can not be removed from memory.
So is super useful to keep your “Services” always reachable and active with Get.find(). Like: ApiService, StorageService, CacheService. [10:38 AM] 엥, getXcontroller에 대한 설명이 readme에 없다. lifecycle에 onInit()가 있어서 가져왔다. [10:40 AM] getXservice는 daemon이라는 얘기고, getXcontroller도 비슷한 lifecycle이라고만… [10:41 AM] code 예제가 있다. class Controller extends GetxController { @override void onInit() { super.onInit(); //Change value to name2 name.value = ’name2’; }
@override void onClose() { name.value = ‘’; super.onClose(); }
final name = ’name1’.obs;
void changeName() => name.value = ’name3’; }
void main() { test(’’’ Test the state of the reactive variable “name” across all of its lifecycles’’’, () { / You can test the controller without the lifecycle, / but it’s not recommended unless you’re not using / GetX dependency injection final controller = Controller(); expect(controller.name.value, ’name1’);
/ If you are using it, you can test everything, / including the state of the application after each lifecycle. Get.put(controller); // onInit was called expect(controller.name.value, ’name2’);
/ Test your functions controller.changeName(); expect(controller.name.value, ’name3’);
/ onClose was called Get.delete<Controller>();
expect(controller.name.value, '');
}); }
hoyoul — 11/10/2023 10:43 AM 해석해보자. [10:43 AM] controller를 생성한다. 그러면 name= ’name1’이다. [10:44 AM] onInit()가 생성시 호출할지 안할지 모르겠다. 설명이 없지? [10:46 AM] onInit은 한번호출되는데 생성할때 호출된다니까, onInit()가 호출 될것이다. [10:47 AM] 그러면 name1 = ’name2’로 바뀔것이다. [10:47 AM] expect(controller.name.value, ’name1’);는 false가 될것이다. expect란 함수가 뭐지..그냥 두값 비교하는거 같긴 한데… [10:48 AM] 내 생각이 false라는 것이지…설명이 없노?ㅋ [10:48 AM] 아…Get.put(controller)할때 onInit이 호출되네. [10:49 AM] oninit()는 생성할때 호출되는게 아니다. getX에 등록될때 호출되는거다. [10:49 AM] 그래서 expect()는 true가 나올것이다. [10:50 AM] 나머지는 다 주석에 적힌대로라서 그대로 이해하면 될듯하다.
hoyoul — 11/10/2023 10:51 AM [주의] GetX에서 GetXController의 onInit()는 Get.put(controller)가 등록될때 호출된다. [10:57 AM] 다시 GameController를 이어서 보자. [10:57 AM] @override void onInit() async { // await initGame(); await KholdemSocket().initSocket(); super.onInit(); }
hoyoul — 11/10/2023 10:58 AM entry point인 main에서 Get.put으로 gamecontroller()를 등록하니까, 위에 있는 멤버 초기화가 된 상태에서 socket을 만든다. [10:58 AM] kholdemSocket을 살펴보자. [10:59 AM] kholdem socket [10:59 AM] import ‘package:get/get.dart’; import ‘package:web_socket_channel/status.dart’ as status; import ‘package:web_socket_channel/web_socket_channel.dart’;
class KholdemSocket { KholdemSocket();
final wsUrl = Uri.parse(‘ws://kholdem.fly.dev/websocket’); var _channel; [11:00 AM] getX package와 web_socket package를 사용한다. [11:00 AM] web_socket_channel 이 뭐지. [11:01 AM] https://pub.dev/packages/web_socket_channel
The web_socket_channel package provides StreamChannel wrappers for WebSocket connections. It provides a cross-platform WebSocketChannel API, a cross-platform implementation of that API that communicates over an underlying StreamChannel, an implementation that wraps dart:io’s WebSocket class, and a similar implementation that wraps dart:html’s.
It also provides constants for the WebSocket protocol’s pre-defined status codes in the status.dart library. It’s strongly recommended that users import this library with the prefix status. [11:03 AM] 이것에 앞서서, socket이 뭔지는 아는데, web socket은 뭐지? [11:04 AM] network 수업에서 socket은 주구장창 사용했지만, web socket을 사용하진 않았다.

Figure 1: websocket1
stack overflow에 설명이 나와 있다. [11:07 AM] 내경험상 socket은 tcp나 udp를 사용할때 사용한다. 그 위에 custom protocol을 만든다. 마치 ftp나 http처럼… [11:07 AM] 그런데 web socket은 http protocol하고 비슷하게 위에 http protocol을 사용하는건 똑같다. [11:08 AM] 그런데 http는 연결하고 끊고, 연결하면 끊는식이라.. 보통 udp를 사용하는데… [11:08 AM] web socket은 tcp를 사용하는거 같다. 물론 내 뇌피셜이다. ㅋ [11:09 AM] 왜냐면 댓글중에도 나와 있지만, 3 hand shake얘기가 나온다. 이게 tcp의 특징이기 때문이다. [11:09 AM] 즉 매우 매우 stable하다. [11:10 AM] 아…그리고 http포트와 같다고 한다. [11:11 AM] 다시 web_socket_channel package를 살펴보자. The web_socket_channel package provides StreamChannel wrappers for WebSocket connections. It provides a cross-platform WebSocketChannel API, a cross-platform implementation of that API that communicates over an underlying StreamChannel, an implementation that wraps dart:io’s WebSocket class, and a similar implementation that wraps dart:html’s.
It also provides constants for the WebSocket protocol’s pre-defined status codes in the status.dart library. It’s strongly recommended that users import this library with the prefix status. [11:12 AM] StreamChannel이란게 있다. 이게 web socket connection의 wrapper라고 한다.
hoyoul — 11/10/2023 11:13 AM 아..우선 websocket을 dart에서 사용하는 방식은 dart.io에 정의된 webSocket이 있고, dart.html에서 제공하는 webSocket이 있다. [11:14 AM] web_socket_channel에서 제공하는 web socket은 StreamChannel이라고 부르는데, 이게 dart.io의 web socket을 wrapping한거다. wrapping은 추가적인 api를 넣어서 만들었다는 뜻이다. [11:15 AM] 그러면 dart.io의 websocket 말고 StreamChannel을 사용하는게 더 탁월한 선택?이다. [11:16 AM] 그리고 socket을 사용할때 status code도 전달을 하는데, status.dart에 status code가 정의되어 있기 때문에 import해서 사용하라고 한다. [11:17 AM] import ‘package:web_socket_channel/web_socket_channel.dart’; import ‘package:web_socket_channel/status.dart’ as status;
main() async { final wsUrl = Uri.parse(‘ws://localhost:1234’) var channel = WebSocketChannel.connect(wsUrl);
channel.stream.listen((message) { channel.sink.add(‘received!’); channel.sink.close(status.goingAway); }); } [11:17 AM] 예제도 있다. [11:18 AM] ws라는 scheme은 뭐지? [11:18 AM] websocket이란 뜻인가 보다. [11:20 AM] 소켓연결이 되면, listener를 달아둔다.
hoyoul — 11/10/2023 11:21 AM message를 받으면, sink하네..음…그냥 다시 received를 상대편에게 보내고, status going away란 status code도 보내고 끝이다. [11:21 AM] message를 처리하진 않는다. 일반적인 stream이라서 해석할껀 딱히 없다. [11:22 AM] 나머지도 읽어보자. [11:22 AM] WebSocketChannel The WebSocketChannel class’s most important role is as the interface for WebSocket stream channels across all implementations and all platforms. In addition to the base StreamChannel interface, it adds a protocol getter that returns the negotiated protocol for the socket, as well as closeCode and closeReason getters that provide information about why the socket closed.
The channel’s sink property is also special. It returns a WebSocketSink, which is just like a StreamSink except that its close() method supports optional closeCode and closeReason parameters. These parameters allow the caller to signal to the other socket exactly why they’re closing the connection.
WebSocketChannel also works as a cross-platform implementation of the WebSocket protocol. The WebSocketChannel.connect constructor connects to a listening server using the appropriate implementation for the platform. The WebSocketChannel() constructor takes an underlying StreamChannel over which it communicates using the WebSocket protocol. It also provides the static signKey() method to make it easier to implement the initial WebSocket handshake. These are used in the shelf_web_socket package to support WebSockets in a cross-platform way. [11:24 AM] 첫번째 문단에서 자화자찬하면서, protocol getter, closeCode getter, closeReason getter가 포함된걸 자랑하는데… [11:25 AM] 각각의 getter가 뭘 말하는진 잘 모르겠다. (edited) [11:27 AM] 두번째 단락을 보자. channel이 stream하고 같은데, sink도 같고 한데…일반적인 stream에서는 sink할때 그냥 sink하는데, 이건 web socket stream이니까, 특별한게 있는데, 그것이 close()로 소켓을 끊을때 socket을 끊은 이유를 closeCode와 CloseReason에 실어 보낼수 있다는 얘기다.
hoyoul — 11/10/2023 11:28 AM 음 그래서 첫번째 단락에서 getter를 사용했구나….여튼 소켓이 끊어진 이유를 알수 있다는 내용이다. (edited) [11:28 AM] 마지막 단락을 보자. [11:35 AM] 마지막 단락은 좀 내용이 많은데, websocketchannel.connect()로 연결해서 사용하는 얘길한다. connect로 연결한다. 그리고 생성자에 stream channel을 생성해서 사용한다고 한다. 즉 stream channel을 사용해서 통신한다는 뜻이고, signKey라는 method가 있는데, 이게 3 handshake하는거라고 한다. 즉 webSocketChannel에 관한 설명을 한다.
hoyoul — 11/10/2023 11:36 AM shelf_web_socket package 란것도 있는데, 얘도 websocket channel을 사용한다고 한다. [11:37 AM] 다시 kholdem을 보자. [11:37 AM] import ‘package:get/get.dart’; import ‘package:web_socket_channel/status.dart’ as status; import ‘package:web_socket_channel/web_socket_channel.dart’;
class KholdemSocket { KholdemSocket();
final wsUrl = Uri.parse(‘ws://kholdem.fly.dev/websocket’); var _channel; (edited) [11:38 AM] rails 서버에 연결한다. [11:39 AM] 위에는 아까 말했듯이 status와 websocketchannel import하고, kholdemSocket()을 생성하는데..이 클래스는 생성자에서 아무것도 안한다. [11:40 AM] 여튼 멤버변수에서 rsUrl로 rails 연결하고…끝이다. [11:41 AM] 여기에 2개의 method가 있다. 봐보자. [11:41 AM] Future<void> initSocket() async { try { _channel = WebSocketChannel.connect(wsUrl);
\_channel.stream.listen((message) {
print(message);
// 임시로 close
\_channel.sink.add('received!');
\_channel.sink.close(status.goingAway);
});
} on Exception catch (e) { e.printError(); } }
Future<void> closeSocket() async { try { _channel.sink.close(status.goingAway); } on Exception catch (e) { e.printError(); } }
Future<void> sendMessage() async { try { _channel.sink.add(‘hello world!’); } on Exception catch (e) { e.printError(); } } [11:41 AM] 3개의 method다. 이건 그냥 자연님이 만든거다. 왜냐면 상속한것도 없고 그냥 class만들어서 사용한건데.. [11:42 AM] 10분간 쉬자. [11:42 AM] 눈아프다.
hoyoul — 11/10/2023 11:51 AM initSocket [11:52 AM] socket을 연결한다. [11:52 AM] wsURL은 이미 rails 서버주소로 매핑되어있다. [11:53 AM] listen()가 있다. 받은 message console에 뿌리고, received를 서버에 보낸다. 그리고 socket을 닫고 닫은 이유를 status.goingAway라고 날려준다. [11:53 AM] status.goingAway가 뭐지, 이게 reason getter인데…뜻은 찾아야 한다.

Figure 2: websocket2
코드에 나와 있다. [11:58 AM] 나같으면 goingaway를 안쓰고 normalClosure를 사용했겠다. 왜냐면 그냥 정상적으로 소켓을 닫은거라서…
hoyoul — 11/10/2023 11:59 AM 근데 왜 init socket에서 socket을 닫았을까? [11:59 AM] 답은 web socket을 사용하지 않기 때문이다. [12:00 PM] 다른것도 보자. [12:00 PM] closeSocket() [12:00 PM] 그냥 닫는 함수다. [12:00 PM] sendMessage() [12:01 PM] hello world를 보낸다. 그런데 연결이 안되었기 때문에…보내지도 않는다. 그냥 껍데기만 만든거임. [12:02 PM] -결론: kholdem web socket은 사용하지 않는다. [12:03 PM] 다시 gameController로 돌아와보자. [12:03 PM] Future<void> initGame() async { // game 초기화 List<GameRoom> room = await KholdemApi().getRooms(); rooms(room); currentRoom(room.first); currentGame(room.first.game); currentPlayerId(currentGame.value.currentPlayerId); users(room.first.users?.toList()); user(room.first.users?.first);
await KholdemSocket().initSocket(); }
Future<void> startGame() async {
Game? result =
await KholdemApi().startGame(GameController.to.currentRoom.value.id!);
if (result != null) {
currentGame(result);
currentPlayerId(result.currentPlayerId);
users(result.users?.toList());
user(result.users
?.where((element) > element.id = user.value.id)
.firstOrNull);
await getGame();
}
}
hoyoul — 11/10/2023 12:06 PM initGame() initGame()는 어디서 호출하지? 왜냐면 onInit은 getXcontroller에서 등록될때 호출되기 때문 websocket을 세팅을 했다. 그런데 안사용한다. [12:07 PM] initGame은 보니까, kholdemApi는 rest_data package사용하는 거 같은데… [12:07 PM] 고칠부분 [12:09 PM] web socket을 사용하기 위해서 onInit()를 사용한다. getXcontroller가 등록될때 oninit()에서 소켓을 초기화하는데, 안사용하고 대신 rest api를 사용한다면 rest api를 초기화하는게 맞다. 아니면 소켓을 사용하던가…여기는 고칠부분이다. [12:10 PM] emacs에서 ref를 사용하는걸 모르겠다. 검색좀 해야겠다.
hoyoul — 11/10/2023 12:20 PM lsp-ui-peek references [12:20 PM] 이것도 hook해놔야 할듯…
hoyoul — 11/10/2023 12:27 PM 밥먹고 와서 다시 분석하자.
hoyoul — 11/10/2023 1:43 PM initGame()는 intro page에서 호출한다. 그러면 main에서 get.put(GameController()); 코드는 의미가 없다. 원래는 websocket을 사용하기 위해서 main에서 호출한건데 websocket을 사용하지 않기 때문에 의미없는 code가 되어버렸다. main에서 mainScreen()를 생성한후에 인증 결과에 따라 intro page를 가고 여기서 initGame()을 마지막에 ui를 만들고 호출하는 형태인데…call flow가 복잡하다. [1:44 PM] 고칠부분 [1:45 PM] call flow를 직관적으로 바꿀 필요가 있다. 아니면 comment로 흐름을 정리할 필요가 있어보인다. [1:46 PM] intro page에서 initGame()을 호출하면, List<GameRoom> room = await KholdemApi().getRooms(); [1:46 PM] 을 호출한다. [1:46 PM] kholdemApi().getRooms()를 살펴보자. [1:47 PM] Future<List<GameRoom>> getRooms() async { try { var gameRooms = await kholdemJsonAdapter.findAll(‘game_rooms’); Iterable<GameRoom> result = gameRooms.map<GameRoom>( (jsonApiDoc) => GameRoom(jsonApiDoc as JsonApiDocument));
return result.map((e) { e.game = e .includedDocs(‘game’, [e.currentGameId!]) .map<Game>((jsonApiDoc) => Game(jsonApiDoc)) .firstOrNull;
e.game?.currentPlayerId = e.game?.currentPlayer['id']?.toString();
e.users = e
.includedDocs('user', e.userIds)
.map<User>((jsonApiDoc) => User(jsonApiDoc));
return e;
}).toList(); } on Exception catch (e) { // TODO : token 만료된 경우, 로그인 화면으로 이동 e.printError(); }
return [];
} [1:48 PM] 궁금한게 KholdemApi().getRooms()는 어떤 의미일까? KholdemApi()를 생성하고 method를 호출하는 것일까? [1:49 PM] 그런거 같다. 익숙하지 않은 형태다. [1:50 PM] 보통 KholdemApi kh = KholdemApi(); kh.getRooms();
hoyoul — 11/10/2023 1:51 PM 이렇게 사용한다. KholdemApi().getRooms()의 형태는 KholdemApi.getRooms()처럼 static method와 비슷하기도 하다. [1:51 PM] 물론 문법적으론 문제 없다. 왜냐면 variable을 안쓰기때문에 더 효율적이라고 볼수도 있다. 그러나 가독성이 안좋은 코드이긴 하다. [1:52 PM] 마치 람다처럼 function의 name을 정할필요가 없기 때문이다. 이렇게 reference가 되는 변수를 안사용한다는건, 여러번 호출이 아닌 once! 딱 한번 호출된다는걸 말하기도 한다. [1:54 PM] KholdemApi class import ‘dart:convert’;
import ‘package:dio/dio.dart’; import ‘package:get/get.dart’; import ‘package:kholdem/api/json/game.dart’; import ‘package:kholdem/api/json/game_room.dart’; import ‘package:kholdem/api/json/round.dart’; import ‘package:kholdem/api/json/user.dart’; import ‘package:kholdem/controller/setting_controller.dart’; import ‘package:kholdem/models/type/act_type.dart’; import ‘package:rest_data/rest_data.dart’;
class KholdemApi { KholdemApi();
// TODO : api factory, business logic 분리 static final Dio kholdemRestApi = _apiInstance(); static final Adapter kholdemJsonAdapter = _adapterInstance(); static final JsonApiSerializer serializer = _serializerInstance(); [1:55 PM] kholdemApi를 생성하기 때문에 생성하는 코드를 살펴보자. [1:56 PM] 한번보면 궁금한건, 왜 static을 사용했을까? 라는 질문이 나온다. [1:57 PM] 이름도 KholdemApi라고 썼는데, static을 써야 할 이유가 있을까? [1:57 PM] dart:convert library를 살펴보자.
hoyoul — 11/10/2023 1:58 PM https://api.dart.dev/stable/3.1.5/dart-convert/dart-convert-library.html
dart:convert library Encoders and decoders for converting between different data representations, including JSON and UTF-8.
In addition to converters for common data representations, this library provides support for implementing converters in a way which makes them easy to chain and to use with streams.
To use this library in your code:
import ‘dart:convert’; Two commonly used converters are the top-level instances of JsonCodec and Utf8Codec, named json and utf8, respectively. [2:00 PM] encoder와 decoder라는 말이 나온다. [2:01 PM] 음…json을 다루는 flutter에서 설명을 보면 encode하고 decode를 원래의 뜻과 좀 다르게 사용한다. [2:02 PM] encode와 decode는 보통 code system의 변환을 말한다. [2:02 PM] 예를 들어서 utf-8로된 set of characters에서 utf-16으로 변환한다던지… [2:03 PM] 그런데 dart 에서 json관련 설명에선 encoding과 decoding을 serializable과 연관지어 설명하는 경우가 많다. 여기서도 그렇다. [2:04 PM] json자체는 javascript object의 준말로, utf-8과 같은 coding system이 아니다. 위에서 말한 data representations가 맞다. [2:04 PM] 일종의 자료구조다.
직렬화(serialization)할때, 객체를 json파일로 하거나 utf8의 byte stream로 뽑아낼수 있다. 이것을 encode라고 한다. [2:07 PM] code화 하는것이다. 일반적으로 encode의 코드변환 용어가 아니고, 객체를 stream으로 뽑아내는걸 encode라고도 하는데, 여기선 후자를 쓴거다. [2:08 PM] convert는 encoding할수도 있고 decoding할수 있는 library라고 보면 된다. 여기서 encode는 객체를 stream으로 뽑아내거나 stream에서 객체를 만들어낼수 있다는 말이다. 이렇게 하는 이유는 통신하기 위함이다. 즉 통신할 때 사용하라는 library다. [2:09 PM] JSON JSON is a simple text format for representing structured objects and collections.
A JsonCodec encodes JSON objects to strings and decodes strings to JSON objects. The json encoder/decoder transforms between strings and object structures, such as lists and maps, using the JSON format.
The json is the default implementation of JsonCodec.
Examples
var encoded = json.encode([1, 2, { “a”: null }]); var decoded = json.decode(’[“foo”, { “bar”: 499 }]’); For more information, see also JsonEncoder and JsonDecoder. [2:10 PM] encode와 decode를 사용하는 방법을 보여준다. [2:10 PM] UTF-8 A Utf8Codec encodes strings to UTF-8 code units (bytes) and decodes UTF-8 code units to strings.
The utf8 is the default implementation of Utf8Codec.
Example:
var encoded = utf8.encode(‘Îñţérñåţîöñåļîžåţîờñ’); var decoded = utf8.decode([ 195, 142, 195, 177, 197, 163, 195, 169, 114, 195, 177, 195, 165, 197, 163, 195, 174, 195, 182, 195, 177, 195, 165, 196, 188, 195, 174, 197, 190, 195, 165, 197, 163, 195, 174, 225, 187, 157, 195, 177]); For more information, see also Utf8Encoder and Utf8Decoder. [2:10 PM] utf-8로 encode와 decode한다. [2:11 PM] 이게 encode와 decode의 원래 뜻을 살린 변환이다. [2:11 PM] ASCII An AsciiCodec encodes strings as ASCII codes stored as bytes and decodes ASCII bytes to strings. Not all characters can be represented as ASCII, so not all strings can be successfully converted.
The ascii is the default implementation of AsciiCodec.
Example:
var encoded = ascii.encode(‘This is ASCII!’); var decoded = ascii.decode([0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, 0x41, 0x53, 0x43, 0x49, 0x49, 0x21]); For more information, see also AsciiEncoder and AsciiDecoder. [2:11 PM] ascii변환이다. [2:12 PM] 즉 이 library는 원래 code system을 변환하는 encoder, decoder에 json을 추가시킨 library다. 그래서 일관적이진 않다. [2:12 PM] 원래 별도로 만드는게 맞다.
hoyoul — 11/10/2023 2:13 PM 고쳐야 할점 [2:14 PM] json을 처리하는데, dart:convert와 rest_data를 두개를 섞어서 쓸 이유가 없다. 왜냐면 dart convert의 변환은 rest_data에 포함되어 있기 때문이다. [2:15 PM] dio package [2:15 PM] https://pub.dev/packages/dio Dart packages dio | Dart Package A powerful HTTP networking package, supports Interceptors, Aborting and canceling a request, Custom adapters, Transformers, etc.
hoyoul — 11/10/2023 2:23 PM A powerful HTTP networking package for Dart/Flutter, supports Global configuration, Interceptors, FormData, Request cancellation, File uploading/downloading, Timeout, Custom adapters, Transformers, etc [2:23 PM] http 패키지라고 한다. [2:24 PM] global configuration? , Interceptors? 모르는 것들이다. [2:24 PM] 계속 읽어보자. [2:24 PM] Get started Install Add the dio package to your pubspec dependencies.
Before you upgrade: Breaking changes might happen in major and minor versions of packages. See the Migration Guide for the complete breaking changes list.
Super simple to use import ‘package:dio/dio.dart’;
final dio = Dio();
void getHttp() async { final response = await dio.get(‘https://dart.dev ’); print(response); } [2:25 PM] 아…이거 버전타는 package다. rails할때도 특정 gem은 버전타듯이…이건 버전을 잘 맞춰야 한다. [2:26 PM] dio에서 get, post를 할수 있나보다. 어쩌면 http package니까 당연하다. 그런데 rest_data가 get post하라고 만든 package인데….이것도 중복같은 느낌이 든다. [2:27 PM] Awesome dio :tada: A curated list of awesome things related to dio.
Plugins Plugins
Welcome to submit third-party plugins and related libraries in here. [2:28 PM] curated? 무슨 뜻이지… [2:28 PM] djective: curated (of online content, merchandise, information, etc.) selected, organized, and presented using professional or expert knowledge. “individuals still desire curated news content” [2:29 PM] 전문가들이 pick한! 엄선된…taster’s choice같은 의미…로 느껴진다. [2:30 PM] dio plugin들이 괜찮은게 많은듯하다. (edited)
Examples # Performing a GET request:
import ‘package:dio/dio.dart’;
final dio = Dio();
void request() async { Response response; response = await dio.get(’test?id=12&name=dio’); print(response.data.toString()); / The below request is the same as above. response = await dio.get( ‘/test’, queryParameters: {‘id’: 12, ’name’: ‘dio’}, ); print(response.data.toString()); } [2:41 PM] rest하고는 좀 다르다. rest는 resource로 접근을 하고 만들때도 resource로 해서 object취급을 하는데, dio는 진짜 http로 page와 값을 가져오는 방식이다. [2:41 PM] rest를 사용하지 않는 http 통신이다. [2:42 PM] Performing a POST request:
response = await dio.post(’/test’, data: {‘id’: 12, ’name’: ‘dio’}); Performing multiple concurrent requests:
response = await Future.wait([dio.post(’/info’), dio.get(’/token’)]); Downloading a file:
response = await dio.download( ‘https://pub.dev/' , (await getTemporaryDirectory()).path + ‘pub.html’, ); [2:42 PM] 깊숙히 안다뤄도 될듯하다. 필요할때 그대로 사용하면 된다. [2:45 PM] get과 post의 차이점은 언뜻 떠오르는건 get은 url에 data가 있고 post는 body에 data가 있는거…또 뭐가 있지..여튼 [2:46 PM] 아..post는 주로 데이터를 서버에 전달할때 많이 사용된다.
그래서 예제를 보면 data를 전달하는 것하고, , 두번째 요청은 데이터없이 요청한다. [2:49 PM] post와 get을 같이 보내는게 좀 특이하다. [2:50 PM] download는 pub.dev/pub.html을 다운받는거 같긴 한데… [2:51 PM] 좀더 정확히는 pub.html이란 이름으로 저장된다가 더 맞겠다. [2:52 PM] Get response stream:
final rs = await dio.get( url, options: Options(responseType: ResponseType.stream), / Set the response type to `stream`. ); print(rs.data.stream); / Response stream. Get response with bytes:
final rs = await Dio().get<List<int>>( url, options: Options(responseType: ResponseType.bytes), / Set the response type to `bytes`. ); print(rs.data); / Type: List<int>. Sending a FormData:
final formData = FormData.fromMap({ ’name’: ‘dio’, ‘date’: DateTime.now().toIso8601String(), }); final response = await dio.post(’/info’, data: formData); [2:53 PM] option을 줘서 stream으로 response를 받는건 특이하다. [2:54 PM] option형태가 많아서 양이 많다. 필요할때 찾아보고…
hoyoul — 11/10/2023 2:58 PM 근데 kholdem에서 만든 것들도 다 json_data나 json_api, dio를 사용하고 있다면 또 같은것을 이렇게 load한다면 중복해서 load는 안할까 하는 생각도 드는데, 알아서 잘 해줄꺼라고 믿는다. [3:04 PM] 다시 돌아가면, kholdemApi라는 class를 다시 보자. [3:04 PM] class KholdemApi { KholdemApi();
// TODO : api factory, business logic 분리 static final Dio kholdemRestApi = _apiInstance(); static final Adapter kholdemJsonAdapter = _adapterInstance(); static final JsonApiSerializer serializer = _serializerInstance();
static Dio _apiInstance() { var dio = Dio(); dio.options.baseUrl = “https://kholdem.fly.dev/api/v1 ”; dio.options.contentType = “application/json”; dio.options.headers[“Authorization”] = “Bearer ${SettingController.to.userToken}”; return dio; }
static Adapter _adapterInstance() { return JsonApiAdapter(‘kholdem.fly.dev’, ‘/api/v1’) ..addHeader(“Authorization”, “Bearer ${SettingController.to.userToken}”); }
static JsonApiSerializer _serializerInstance() { return JsonApiSerializer(); } [3:05 PM] static 변수와 이를 사용하는 static methods로 이루어졌다. 아..이게 pattern인데…기억이 안난다. [3:05 PM] 여튼 이 class는 외부에 제공할려는 목적으로 사용되는 class란걸 말해준다.
hoyoul — 11/10/2023 3:06 PM factory pattern을 사용했나보다. 기억도 안난다. [3:08 PM] 아..이거 어렵게 짰다. [3:09 PM] rest와 dio를 섞어서 쓸뿐만 아니라, get으로 controller를 사용하는것도 to를 사용해서 controller를 가져오는 것도 복잡하게 만들어준다. [3:12 PM] 우선 생성자에서는 dio를 사용한다.
hoyoul — 11/10/2023 3:13 PM option을 사용하는데, 여기에 SettingController.to.userToken을 사용한다. controller의 token을 가져와서 사용한다. [3:14 PM] 그런데, _adapterInstance는 rest_data package를 사용한다. [3:15 PM] rails에서 rest와 그냥 http통신이 섞여져 있기 때문에 그것에 맞출려고 여기서도 그런것일수 있다. [3:16 PM] _adapterInstance()는 resource에 대한 adapter를 만든 것이다. [3:19 PM] 그전에 dio.options.headers[“Authorization”] = “Bearer ${SettingController.to.userToken}”; [3:20 PM] 이코드를 해석해야 한다. 물론 그냥 아 token을 전달하는구나..알순 있지만, 문법이 익숙하지 않기 때문이다.
hoyoul — 11/10/2023 3:21 PM 아…머리아프다. 좀 쉬자.
hoyoul — 11/10/2023 3:36 PM 우선 dio.options.headers[“Authorization”]은 dio로 요청하는 모든 header에 Authorization이 붙는다. [3:37 PM] Authorization: Bearer SettingController.to.userToken형태로 header에 붙는다. [3:38 PM] Bearer는 oauth token임을 알려주는 것이다. [3:38 PM] SettingController.to.userToken을 보자. 예상은 된다. 왜냐면 controller를 이렇게 가져오게 to를 사용해서 처리했다. [3:40 PM] 이렇게 controller를 가져오는 방식이 getX의 방식과 다르기때문에 고쳐야 한다. [3:40 PM] 소스를 보면 다음과 같다. [3:40 PM] class SettingController extends GetxController { static SettingController get to => Get.find();
var googleUser = Rxn<GoogleSignInAccount?>(null); var userToken = Rxn<String?>(null); var userEmail = Rxn<String?>(null);
hoyoul — 11/10/2023 3:46 PM 위와같이 controller에 있는 userToken을 접근하는 방식이다. [3:50 PM] static Adapter _adapterInstance() { return JsonApiAdapter(‘kholdem.fly.dev’, ‘/api/v1’) ..addHeader(“Authorization”, “Bearer ${SettingController.to.userToken}”); }
static JsonApiSerializer _serializerInstance() { return JsonApiSerializer(); }
Future<List<GameRoom>> getRooms() async { try { var gameRooms = await kholdemJsonAdapter.findAll(‘game_rooms’); Iterable<GameRoom> result = gameRooms.map<GameRoom>( (jsonApiDoc) => GameRoom(jsonApiDoc as JsonApiDocument));
return result.map((e) { e.game = e .includedDocs(‘game’, [e.currentGameId!]) .map<Game>((jsonApiDoc) => Game(jsonApiDoc)) .firstOrNull;
e.game?.currentPlayerId = e.game?.currentPlayer['id']?.toString();
e.users = e
.includedDocs('user', e.userIds)
.map<User>((jsonApiDoc) => User(jsonApiDoc));
return e;
}).toList(); } on Exception catch (e) { // TODO : token 만료된 경우, 로그인 화면으로 이동 e.printError(); }
return [];
} [3:51 PM] getRooms()는 static이 아니다. [3:52 PM] member 함수다. [3:52 PM] 여기서는 rest_data package를 사용한다.
hoyoul — 11/10/2023 3:53 PM adapter를 사용해서 findAll로 game rooms의 모든 레코드를 가져온다. [3:55 PM] adapter가 가져온 jsonApiDoc를 gameroom객체로 만들고 iterable로 만들어 result에서 참조한다. [3:58 PM] 받아온 gameroom을 처리하는데, chain rule로 처리한다. [3:59 PM] return result.map((e) { e.game = e .includedDocs(‘game’, [e.currentGameId!]) .map<Game>((jsonApiDoc) => Game(jsonApiDoc)) .firstOrNull;
e.game?.currentPlayerId = e.game?.currentPlayer['id']?.toString();
e.users = e
.includedDocs('user', e.userIds)
.map<User>((jsonApiDoc) => User(jsonApiDoc));
return e;
}).toList(); } on Exception catch (e) { // TODO : token 만료된 경우, 로그인 화면으로 이동 e.printError(); }
hoyoul — 11/10/2023 4:01 PM gameroom객체로부터 game을 뽑아내는거 같다. [4:03 PM] gameroom객체에 includeDocs()가 있는지 찾아보자. [4:04 PM] Iterable<JsonApiDocument> includedDocs(String type, [Iterable<String>? ids]) => jsonApiDoc.includedDocs(type, ids);
hoyoul — 11/10/2023 4:12 PM gamerooms는 iterable의 객체다. 쉽게 말해서 list로 객체가 전달되면, 그것을 mapping해서 e.game으로 만드는데, 조건이 있다. currentGameId가 있는것만 game객체로 만드는 것이다. 여기서 이해가 안가는게, map할때 jsonApiDoc을 사용하는것과 firstOrNull이 뭔지 모르겠다. [4:12 PM] 느낌이 open source분석하는 느낌이네.
hoyoul — 11/10/2023 4:21 PM 물론 Gameroom객체에 위와같은 함수가 있다. includedDocs를 호출하면 jsonApiDoc이 나온다. 이것은 jsonApiDocument를 말하는데, json문서를 include만 뽑아서 객체로 만든것이다. [4:22 PM] 그래서 현재 currentID:게임하고 있는 방의 game객체를 얻겠다는 뜻이다. [4:23 PM] .firstOrNull은 dart iterable에 있는 메소드라고 한다. [4:24 PM] 즉 currentGame은 iterable로 받는데, 예상하듯 currentgame이 없을수도 있고 여러판을 할수 있다면, 여러개가 나올수 있을것이다. [4:25 PM] 보통은 하나의 게임만 한다고 생각하기 때문에 FirstOrNull을 사용해서 여러게임이 있어도 첫번째 게임만 가져오겠다는 뜻이다. [4:26 PM] e.game?.currentPlayerId = e.game?.currentPlayer[‘id’]?.toString();
hoyoul — 11/10/2023 4:36 PM user가 현재 하는 게임에서 playerid를 얻어온다. 이것은 game방에서 사용하는 id같다. [4:37 PM] e.users = e .includedDocs(‘user’, e.userIds) .map<User>((jsonApiDoc) => User(jsonApiDoc)); return e; }).toList(); } on Exception catch (e) { // TODO : token 만료된 경우, 로그인 화면으로 이동 e.printError(); }
return [];
} [4:38 PM] 나머지를 보면, e는 현재 play하는 currentGame객체다. 여기서 game하고 있는 사용자들을 뽑아내는 코드다. [4:40 PM] 즉 현재 game에서 game, users, currentPlayerId를 세팅해서 다시 return하는데, list로 리턴을 한다. [4:41 PM] firstOrNull로 current game 의 첫게임만 가져오는거 아닌가? 그러면 list가 아닌 Future<GameRoom>을 return하는게 맞지 않나? (edited) [4:42 PM] 게임이 하나인걸 list로 보내나 GameRoom instance로 보내나 동작에는 문제가 없는데, 내가 이해한게 잘못된거 아닌가 하는 생각이 든다. [4:42 PM] 이거 디버깅으로 꼭 체크해야 한다.
hoyoul — 11/10/2023 4:43 PM 어차피 지금은 code분석하면 code를 이해하는데 중점이 있다. code flow나 이상한것은 debugging하면서 이해를 해야한다. 지금은 말그대로 코드자체 이해에 목적이 있다.
hoyoul — 11/10/2023 4:56 PM 밥먹고 저녁때 다시보자.
11.11
code flow에서 json처리 부분과 getX의 처리부분이 겹쳐져 있다고 생각한다. json을 읽어와서 객체로 변환하는 작업들은 business logic이라서 stream처리로 해결하고, 끝난후에 controller를 만들어 view와의 getX처리를 하는 방식으로 해서 getX를 명확하게 하는 식이 좀 더 직관적일꺼 같다. 아직은 코드가 낯설은거 같다. 좀 더 코드에 익숙해지면 이 부분에 대한 리팩토링이 이뤄질 거 같다. (edited)
hoyoul — 11/11/2023 5:06 AM emacs에서 babel-tangle이 안되고 있다. 다시 코드를 봐야할듯하다.
hoyoul — 11/11/2023 6:22 AM kholdem 코드가 복잡한 이유는, getx의 controller를 page의 model을 관리하는 관점으로 사용하고 있지 않기 때문이다. 상태데이터를 stateful page에서 controller를 빼와서 사용하는게 아니라, observer pattern으로 사용한다. 그래서 json에 해당하는것을 모두 obs로 걸고, 값이 변하면 객체의 값도 변하고 controller의 값도 변하게 했다. 그런데, getx는 이렇게 사용하면 안된다. rest api와 getX의 controller를 연결해서 사용하면 안되고, getConnect를 사용해야 한다. 내가 궁금한건, 자연님이 GetX를 알고 이렇게 코드를 작성했는가? 아니면 모르는 채로 사용했는가? 아직까진 확신이 들지 않는다.

Figure 3: getx2
getX문서에는 그래서 GetConnect를 제공한다. 아니면 stream을 사용했으면 어땠을까? 하는 생각이 든다.
hoyoul — 11/11/2023 6:33 AM GameController분석을 다시 하겠지만, 우선 GameController까지 가기 과정부터 모두 GetX를 제대로 사용하지 못하고 있다. 주말에 코드를 완전 분석해야겠다. kholdem 다시 처음부터 (edited) [6:35 AM] 처음 시작인 main.dart에서 2가지 코드를 눈여겨 봐야 한다. [6:35 AM] @override Widget build(BuildContext context) { return GetMaterialApp( [6:37 AM] app을 Getx로 관리하는 GetMaterialApp을 사용하겠다고 한다. 이말은 모든 app에 사용되는 page에서 model을 view에서 빼내겠다는 뜻이다. 상태 데이터를 예전에는 stateful로 만든 page에서 관리 했었지. 이제는 그렇게 안해, page는 모두 stateless로 만들꺼야 그리고 상태 data는 GexController에서 관리하겠다는 뜻이다. [6:38 AM] 이런 상황에서 home page, 그러니까 제일 처음 사용하는 page가 MainScreen인데, 이것이 stateful이다. (edited) [6:38 AM] ), home: const MainScreen(), ); [6:39 AM] 어…이상한데,…그래 어찌되었던 진행해 보자.라고 해서 진행했다. [6:40 AM] MainScreen Page는 간단하다. [6:40 AM] class MainScreen extends StatefulWidget { const MainScreen({Key? key}) : super(key: key);
@override State<MainScreen> createState() => _MainScreenState(); }
class _MainScreenState extends State<MainScreen> { @override void initState() { super.initState(); }
@override Widget build(BuildContext context) { return SafeArea( child: Obx(() => Scaffold( body: SettingController.to.isValidToken() ? const IntroPage()
const LoginPage(),
)));
} }
hoyoul — 11/11/2023 6:44 AM obx()를 사용해서 page widget을 만든다. 이런 경우는 없다. GetX는 모든 page를 새로 만든다. 그래서 stateless다. 상태 데이터를 controller로 빼왔기 때문에 page를 새로 만들때, 상태 data와 관련된 widget만 obx()를 사용해서 만든다. 이것처럼 전체 page를 다시 만들겠다면서 obx를 사용한것은 stateful을 사용할 필요도 없는것이다. stateful로 만든다는 것은 상태를 내부적으로 관리하겠다는 뜻인데, obx로 외부에서 관리하는 함수를 쓴것이다. 이게 refactoring되어야 한다. refactoring (1) stateful -> stateless class MainScreen extends StatefulWidget => class MainScreen extends StatelessWidget
(2) obx()를 상태에 따라 변화하는 widget만 만든다. (edited) [6:50 AM] 여기서 코드를 진행하면 SettingController.to.isValidToken()를 사용해서 ValidToken()를 호출한다. 이것은 login이 이미 된 상태냐, 아니면 처음 앱을 시작해서 login을 안한 상태냐를 물어본다. GetX의 controller의 함수를 호출하는 건데, 당연히 이건 GetX를 이상하게 사용하는 code다. isValidToken()에는 2개의 값을 검사한다. token과 email이다. login이 되었다면 두개의 값이 있어서 true가되고 아니면 false가 된다. code를 보자. [6:50 AM] bool isValidToken() { // 토큰 유효성 검사 -> TODO : 유효기간 체크 return (userToken.isNotEmpty ?? false) && (userEmail.isNotEmpty ?? false); } [6:51 AM] Token과 email이 있다면, login했기 때문에 IntroPage로 간다. 처음하면 login page로 간다. 우리는 처음하기 때문에 login page를 분석해야 한다. 여기서 refactoring을 해야 한다.
hoyoul — 11/11/2023 6:59 AM Refactoring (1) SettingControll에서 모든걸 다하는데, 그러면 안된다. 우선 LoginController를 만들고, obs를 걸 두개의 상태데이터만 생성해서 userToken, userEmail에 obs를 건다. 그리고 Get.put(LoginController)를 등록한다. 그래야만 view에서 controller를 꺼내서 쓸수 있다. class LoginController extends GetXController{ String userToken = ‘’.obs; String userEmail = ‘’.obs; } Get.put(LoginController);
(2) MainScreen에선 scaffold를 obx로 감싸면 안된다. 이전에 email과 usertoken을 LoginController에로 GetX에 등록했기 때문에, LoginController를 가져와서 그 값이 변하면 page를 만드는 방식으로 해야한다. obx(()=>{ final Controller<LoginController> c = Get.find(); /generic은 지원되는 지 확인요. if (c.userToken && c.userEmail) { / userToken과 userEmail이 모두 true인 경우 createIntroPage(); } else { // userToken 또는 userEmail 중 하나라도 false인 경우 createLoginPage(); } }
(edited) [7:03 AM] 이정도만 되도 코드를 이해할 수 있다. 여튼 계속 진행하자. 처음이니까 loginPage로 이동할 것이다. (edited)
hoyoul — 11/11/2023 7:11 AM Login Page 분석 source code를 discord에서 길이제한을 했다. 길이가 길어지면 유료로 해야 한다. 따라서 두번에 나누어 분석한다. (edited) [7:13 AM] import ‘package:flutter/material.dart’; import ‘package:kholdem/controller/setting_controller.dart’; import ‘package:kholdem/view/intro.dart’;
class LoginPage extends StatelessWidget { const LoginPage({super.key});
@override Widget build(BuildContext context) { return Scaffold( body: Center( child: SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 30), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( child: const Text( ‘Log in or sign up’, style: TextStyle( color: Colors.white, fontWeight: FontWeight.w900, fontSize: 25), ), ), Container( child: const Text( ‘Get a K-holdem account and find your\n’ + ‘joy whenever you—’, style: TextStyle( color: Colors.grey, fontWeight: FontWeight.w700, fontSize: 17), ), ) ], ), ), [7:16 AM] const SizedBox( height: 50, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 30), child: ElevatedButton( onPressed: () async { bool result = await SettingController.to.handleGoogleSignIn();
if (!context.mounted) return;
if (result) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const IntroPage()),
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
shadowColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
padding: const EdgeInsets.symmetric(vertical: 10)),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Continue with Google',
style: TextStyle(color: Color(0xff272731)),
),
[7:17 AM] const SizedBox( width: 10, ), Image.asset( ‘assets/images/icons/google.png’, width: 30, ), ], )), ) ], ), ), ), ); } }
hoyoul — 11/11/2023 7:24 AM view page에 Controller가 있다. GetX를 사용하는 이유는 상태 data와 Controller를 모두 GetXController에 넣어서 관리하는 데 있다. 그런데 view에서 controller를 직접 사용하고, GetX의 routing을 사용하지 않고, navigator를 사용한다. GetX를 사용하는 이유가 2가지가 있는데, 첫번째가 상태관리를 Controller에서 하겠다. 두번째 page routing이다. 둘다 무시한다. 여기서 결론, 자연님은 GetX를 모른다.가 확실해졌다.
child: ElevatedButton( onPressed: () async { bool result = await SettingController.to.handleGoogleSignIn();
if (!context.mounted) return;
if (result) { Navigator.push( context, MaterialPageRoute( builder: (context) => const IntroPage()), ); } },
refactoring (edited) [7:27 AM] (1) 여기서 onPressed할때 controller를 직접 사용한다. GetX에서 controller를 가져오게 해야 한다. 그런데 상태 데이터가 뭔지 확인해야 한다. 그리고 상태데이터로 관리 해야 하나? 도 생각해야 한다. 상태데이터가 ui를 변경시킬 데이터인지, 아니면 그냥 observer로 관리해서 값이 변하면 다른일을 해야하는 데이터인지를 구분해야 한다. 만일 ui를 변경시킨다면 obx를 쓰는것이고, 값의 변경여부만 사용한다면 ever를 써야 하기 때문이다. 그러면 elevatebutton을 누르는 목적이 무엇인가? 단순히 데이터의 변경인가? 아니면 상태 데이터의 변경인가? 상태 데이터라면, 상태 데이터를 관찰해서 무언가를 할것인가? 모르겠다. login에서 결과값을 ui에 뿌려주지 않기 때문에 상태 데이터가 아닌거 같다. login해서 그 결과는 token인데, token은 ui를 변경시키지 않는다. 단지 server에 전달하기 때문에, 상태 데이터가 아니다. 그냥 data다. 그리고 observable해야할 data도 아니다.만일 refresh token이라면 observable해야겠지만, 잘 모르겠다. (edited) [7:27 AM] handleGoogleSignIn()를 보자. [7:28 AM] Future<bool> handleGoogleSignIn() async { try { GoogleSignInAccount? account = await _googleSignIn.signIn(); if (account != null) { googleUser(account);
var auth = await account.authentication; bool result = await KholdemAuthApi() .login(auth.accessToken, auth.idToken, account.serverAuthCode);
return result;
} } catch (error) { error.printError(); }
return false; }
hoyoul — 11/11/2023 7:35 AM account와 auth 둘다 controller로 빼내야 한다. 위에서 googleUser는 obs와 같은 Rxn변수다. 이것도 좀 이상한 코드다. 상태관리를 다시 reactive native management변수를 사용? 일관성이 없다. 여튼 상태관리를 해야한다고 자연님은 판단한듯 하다. ui가 변경되어야 하는 상태 데이터인지는 잘 모르겠다. 여튼 나는 refresh token에만 obs관리를 할것이고, 일반 계정은 그냥 data로 본다. 그리고 저렇게 view에서 작업을 하면 안된다. refactoring (edited)
hoyoul — 11/11/2023 8:38 AM class AuthController extends GetXController{ GoogleSignInAccount account; String? accessToken = ’ ‘.obs; //access token이 refresh token이라면 observable. String? idToken; String? ServerToken;
var auth;
void connectGoogle() { account = await googleSignIn.signIn(); if (account ! = null){ auth = await account.authentication; try{ accessToken = auth.accessToken; idToken = auth.idToken; serverToken = account.serverAuthCode; } catch(e){ e.print(“error”); } } / json형태로 server로 3개의 code를 보내는것은 별도의 함수로 처리 / sendToken(accessToken, idToken, serverToken);
_/ page이동
Get.to(createIntroPage());
} int sendToken(String? at, String? id, String? st){ /_ server에 전송 }
// AccessToken을 감시하고 있다가 값이 변경되면 콜백 실행, 언제,누가 refresh하는지를 몰라서 우선. ever(accessToken, (_) { print(“GoogleSignInAccount changed: ${accessToken.value}”); }); } Get.put(AuthController()); (edited) [8:39 AM] 자연님 코드에선 KholdemAuthApi().login함수에서 google에서 받은 token을 server로 바로 보낸다. 하지만, refresh token에 대한 처리도 필요하다. 그래서 나는 분리했다. refreshtoken은 실제 존재하는지 안하는지도 모른다. 그런데 있다고 치고, 있다면 누가 언제 주는지도 모른다. 만일 내가 update해야 한다면, update함수를 만들어서 처리해야한다. 그리고 자연님 코드가 복잡하다. 해야할 것은 3가지고 3가지 모두 controller가 해야하는 일들이다. controller에서 처리하고 view는 화면관련 내용만 있어야 한다. code의 logic (1) google에서 token받아오기 (2) server에 json으로 보내기 (3) intropage로 이동 (edited)
hoyoul — 11/11/2023 8:47 AM 나는 위의 logic으로 controller에 넣었다. 그리고 view에서 사용해야한다. 그래서 refactoring이 필요하다. (edited)
hoyoul — 11/11/2023 8:55 AM child: ElevatedButton( onPressed: () async { final Controller c = Get.find(); c.connectGoogle(); }, [8:55 AM] 이렇게 간단하게 고쳐야 한다. view에서는 ui만 하는것이다. [8:57 AM] 나머지는 ui이니까, 그대로 사용하면 된다. [8:57 AM] login이 성공되었다고 하면 intro page로 넘어가게 된다. [8:57 AM] IntroPage 분석 [8:58 AM] 나는 createIntroPage()로 처리했는데, 상관없다. 함수던 class던… intro page에 보면 ui관련없는 package들이 쫙 import되어 있는데, 저거다 제거해야 한다. 그리고 stateful도 제거해야 한다. 다시 짜야 하는거다. page를 보면 알겠지만, 기본적으로 page하나에 이렇게 많은 코드가 들어간다는 것부터 문제가 있는 코드다. 이렇게 긴 코드는 모듈화를 안했던가 business logic이 한데 뒤엉킨 스파게티소스일때이다. (edited) [8:59 AM] import ‘package:flutter/material.dart’; import ‘package:get/get.dart’; import ‘package:kholdem/api/json/user.dart’; import ‘package:kholdem/config/app_color.dart’; import ‘package:kholdem/controller/game_controller.dart’; import ‘package:kholdem/controller/setting_controller.dart’; import ‘package:kholdem/view/holdem.dart’;
class IntroPage extends StatefulWidget { const IntroPage({super.key});
@override State<IntroPage> createState() => _IntroPageState(); } [9:01 AM] class _IntroPageState extends State<IntroPage> {
@override Widget build(BuildContext context) { return Scaffold( backgroundColor: colorMain, body: Obx(() => Center( child: ListView( shrinkWrap: true, children: [ Stack( children: [ Container( margin: const EdgeInsets.symmetric(horizontal: 40), padding: const EdgeInsets.symmetric(vertical: 40), decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.all(Radius.circular(15.0)), ), child: Column( children: [ Text( ‘2 CARD PLAY’, style: TextStyle( fontWeight: FontWeight.w900, fontSize: 30, color: Colors.white, ), ), SizedBox( height: 10, ), GameController.to.rooms.isEmpty ? Text(‘Loading…’)
Text(
'User Email : ${SettingController.to.userEmail}\n'
'Game Room ID : ${GameController.to.rooms.first.id} / State : ${GameController.to.rooms.first.state}\n'
'Game Id : ${GameController.to.currentGame.value.id} / Game State : ${GameController.to.currentGame.value.state}'),
SizedBox( height: 10, ),
hoyoul — 11/11/2023 9:03 AM Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Container( width: 36, child: Stack( children: [ Positioned( child: CircleAvatar( radius: 8, backgroundColor: colorMain.withOpacity(0.9), child: CircleAvatar( backgroundColor: Colors.white, radius: 6, )), left: 22, ), Positioned( child: CircleAvatar( radius: 8, backgroundColor: colorMain.withOpacity(0.9), child: CircleAvatar( backgroundColor: Colors.white, radius: 6, )), left: 11, ), [9:03 AM] Positioned( child: CircleAvatar( radius: 8, backgroundColor: colorMain.withOpacity(0.9), child: CircleAvatar( backgroundColor: Colors.white, radius: 6, )), ) ], ), ), SizedBox( width: 10, ), Text( ‘${GameController.to.rooms.isNotEmpty ? GameController.to.rooms.first.userIds.length - 1 : ‘’} Other Plays wating’, style: TextStyle( fontSize: 12, color: Colors.white60, ), ), ], ), SizedBox( height: 50, ), SizedBox( height: 200, ), ], ), ), Positioned( child: Container( child: Image.asset(‘assets/images/main_img.png’), margin: const EdgeInsets.only(left: 40), / width: MediaQuery.of(context).size.width * 0.8, height: 200, ), left: 50, bottom: 40, ) ], ), SizedBox( height: 10, ), [9:04 AM] Center( child: DropdownButton<User>( value: GameController.to.user.value, icon: const Icon( Icons.arrow_downward, color: Colors.white, ), elevation: 16, style: const TextStyle(color: Colors.black), onChanged: (User? value) { GameController.to.user(value); }, items: GameController.to.users .map<DropdownMenuItem<User>>((User value) { return DropdownMenuItem<User>( value: value, child: Text( value.name, style: TextStyle(color: Colors.black), ), ); }).toList(), ), ), Center( child: ElevatedButton( style: ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.white, padding: const EdgeInsets.symmetric( vertical: 15, horizontal: 30), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30.0), ), textStyle: const TextStyle(fontWeight: FontWeight.w700), ), onPressed: () async { / TODO : Select Room await GameController.to.getGame();
Navigator.push( context, MaterialPageRoute( builder: (context) => const Kholdem()), ); },
[9:04 AM] child: Text( ‘Start’, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 25, color: colorMain, ), )), ) ], ), )), ); }
@override void initState() { super.initState(); GameController.to.initGame(); } }
hoyoul — 11/11/2023 9:46 AM Refactoring [9:48 AM] (1) stateful을 stateless로 바꾼다. getX가 하는게 나 stateful 안써, data는 따로 관리할꺼야. 그렇기 때문에 stateful을 쓰지 않는다. [9:51 AM] (2) body에서 obx로 Center를 만든다. GetX에도 나와 있듯이 obx로 widget을 만드는데 하나의 규칙?이 있다. 상태데이터가 관여되는 widget을 obx로 만든다. 이렇게 넓게 다 만들지 않는다. 상태 data가 무엇인지 확인하고, 관련 widget을 obx로 만들어야 한다. body: Obx(() => Center( child: ListView( shrinkWrap: true, children: [ [9:52 AM] 위에 코드를 refactoring할려면, 관련된 상태 data는 무엇일지부터 확인해야 한다. (edited)

Figure 4: card1
2가지다. 이 2가지 코드를 view에서 처리하는데, 이것을 refactoring해야 한다. [10:24 AM] 첫번째를 보자. [10:24 AM] (3) 코드는 다음과 같다. SizedBox( height: 10, ), GameController.to.rooms.isEmpty ? Text(‘Loading…’)
Text(
'User Email : ${SettingController.to.userEmail}\n'
'Game Room ID : ${GameController.to.rooms.first.id} / State : ${GameController.to.rooms.first.state}\n'
'Game Id : ${GameController.to.currentGame.value.id} / Game State : ${GameController.to.currentGame.value.state}'),
SizedBox( height: 10, ),
hoyoul — 11/11/2023 10:25 AM 이것은 text만 표시한건데, 아래 circle은 이어서 설명을 할것이다. [10:28 AM] 보여지는 data는 UserEmail, GameRoomID, State, GameID, GameState이다. 이것이 상태 데이터다. [10:30 AM] Controller를 만들어야 한다. 위에선 SettingController, GettingController여러개를 static으로 가져오는데, 이것은 자연님이 GetX 사용법을 몰라서 이렇게 쓴거다. (edited) [10:30 AM] refactoring이 필요하다. [10:30 AM] Controller를 만들어야 한다. 사용되는 data를 어디서 가져오는지 확인하고 만들어야 하기 때문에, UserEmail: login하고 sharedPreference에 저장한걸 가져온다. GameRoomID: rails서버로 부터 가져온다. 복잡하게 가져온다. ㅠㅠ, JsonApiAdapter(‘kholdem.fly.dev’, ‘/api/v1’). => 이것도 refactoring해야 한다. State: GameRoomID와 동일한 과정 Game id: current game room정보인데 rails서버로부터 가져온다. Game State:game room정보인데 rails server로 가져온다. rest api를 초기화해서 https://kholdem.fly.dev/api/v1 에서 gamerooms로 가져오는데, 확인해보자. (edited) November 12, 2023
11.12
refactoring할께 좀 많다. 그리고 혼란스럽다. 혼란스러운 이유는 rest api와 json통신?을 섞어쓰고, 관련된 package도 여러개 사용한다는 점을 들수 있겠다. [11:18 AM] json_api, rest_data, dio, http…등등, 여기에 web socket도.. [11:18 AM] 하나만 정의해서 사용하면 안되나? 하는 생각이 든다. 목적은 서버로부터 데이터를 가지고 오고 보여주고, 변경된 데이터를 다시 서버로 보내주는데, http를 사용할뿐이다.
hoyoul — 11/12/2023 11:19 AM http를 사용하는 방식은 rest api로 마치 rmi사용하듯이 사용할 수도 있고, http의 get post를 사용할 수도 있고, web socket으로 데이터를 주고 받을 수도 있고…json문서를 주고 받을수도 있다. [11:20 AM] 많은 시도가 있었나보다. [11:20 AM] 근데 보는 입장에선 좀 혼란스럽긴 하다. [11:22 AM] refactoring할껏만 적고 넘어가자. 위에서 말했듯이 Controller를 만들어서 처리해야 하고, 두번째로 http통신을 별도의 class로 만들어야 한다. [11:22 AM] http 통신 클래스의 작성
hoyoul — 11/12/2023 11:28 AM ui에서 데이터를 보낼일이 있으면 그냥 http통신 class만들고, fetchData()같은거 만들어서 데이터 가져와서 사용하면 된다. 만일 GetxController의 상태데이터로 만들어도 되지만 분리해서 해야한다. getx로 관리 안해도 상관없다. 왜냐면 ui에서 통신해서 가져온 데이터를 보여주기만 하면 된다. 예를 들어서 intro화면에서 사용자나,game정보를 보여주는데 이것은 fetchData()에서 http통신(어떤게 되었던 상관없다)을 통해서 gameroomid, state, gameid,game state를 가져와서 보여주면된다. [11:29 AM] 상태데이터로 관리한다면 http클래스에서 controller에 있는 상태데이터를 업데이트만 해주면 된다. [11:30 AM] 그리고 UI에서 Text로 보여주는 정보에서는 obx()로 widget을 만들면 된다. [11:32 AM] 우선 넘어가자. 우선 내가 해야할 것은 check만 하는 것이다. 지금 전체를 뜯어 고칠순 없다. 소스를 이해하고 아 이런식으로 처리했네, 그리고 그것에 맞춰서 해야할 것을 처리해보는 것이다. 그리고 나중에 시간될때, 소스를 변경해야 하는데… [11:33 AM] 이런 코드는 스파게티라서…좀 스트레스를 받는다.ㅋ
hoyoul — 11/12/2023 11:36 AM total 게임 참여자 표시 (edited) [11:39 AM] SizedBox( width: 10, ), Text( ‘${GameController.to.rooms.isNotEmpty ? GameController.to.rooms.first.userIds.length - 1 : ‘’} Other Plays wating’, style: TextStyle( fontSize: 12, color: Colors.white60, ), ), ], ), SizedBox( height: 50, ), [11:39 AM] 이것도 business logic이 분리가 안됐다. [11:42 AM] 첫번째 게임방에 있는 사람들의 수를 출력한다. [11:42 AM] refactoring [11:42 AM] getX로 고치던, http로 고치던 해야 한다. [11:42 AM] controller를 제거해야 한다.
hoyoul — 11/12/2023 11:44 AM 사람이름 선택 (edited) [11:45 AM] ``` Center( child: DropdownButton<User>( value: GameController.to.user.value, icon: const Icon( Icons.arrow_downward, color: Colors.white, ), elevation: 16, style: const TextStyle(color: Colors.black), onChanged: (User? value) { GameController.to.user(value); }, items: GameController.to.users .map<DropdownMenuItem<User>>((User value) { return DropdownMenuItem<User>( value: value, child: Text( value.name, style: TextStyle(color: Colors.black), ), ); }).toList(), ), ), (edited) [11:45 AM] GameController 제거해야 한다. [11:46 AM] 이것도 동일하다. getX를 몰라서 사용되는 오류다. [11:46 AM] 그냥 데이터 가져와서 보여주는거라서 분석은 필요 없다. [11:46 AM] 동일한 refactoring이 필요하다. [11:47 AM] start버튼 (edited) [11:50 AM] Center( child: ElevatedButton( style: ElevatedButton.styleFrom( foregroundColor: Colors.white, backgroundColor: Colors.white, padding: const EdgeInsets.symmetric( vertical: 15, horizontal: 30), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(30.0), ), textStyle: const TextStyle(fontWeight: FontWeight.w700), ), onPressed: () async { // TODO : Select Room await GameController.to.getGame();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const Kholdem()),
);
},
child: Text(
'Start',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 25,
color: colorMain,
),
)),
)
],
),
)), );
hoyoul — 11/12/2023 11:54 AM start 버튼을 누르면 방을 골라야 한다. 이것을 Todo로 표시했다. [11:55 AM] view에서 처리하는거 다 옮겨야한다.(refactoring) [11:56 AM] GetX를 사용하면서 navigator를 써서 이동한다. 즉 getX를 모르는건데…이럴바엔, GetX를 사용하면 안된다. GetX와 stateful코드가 짬뽕되어 있다. 하나만 하면 된다. GetX를 사용하지 않고, stateful과 stream을 적절히 섞어 쓰던가, 아니면 GetX를 쓰던가…GetX로 바꿔야 한다.(refactoring) [11:58 AM] Kholdem()이란 page가 game page같은데, naming도 명시적이였으면 좋겠다. kholdem은 app의 이름과 같아서 game화면이란 느낌이 안든다. [11:58 AM] 아마도 stateful로 썼을꺼 같다. [11:58 AM] Kholdem Page 분석 [11:59 AM] import ‘package:flutter/material.dart’; import ‘package:get/get.dart’; import ‘package:kholdem/controller/game_controller.dart’; import ‘package:kholdem/widget/betting_block.dart’; import ‘package:kholdem/widget/button/act_button_group.dart’; import ‘package:kholdem/widget/hud.dart’; import ‘package:kholdem/widget/order_hint.dart’; import ‘package:kholdem/widget/slot/empty_slot.dart’; import ‘package:kholdem/widget/slot/my_slot.dart’; import ‘package:kholdem/widget/slot/user_slot.dart’;
class Kholdem extends StatefulWidget { const Kholdem({super.key});
@override State<Kholdem> createState() => _KholdemState(); }
class _KholdemState extends State<Kholdem> {
var users = GameController.to.users
.where((u) > u.id ! GameController.to.user.value.id);
hoyoul — 11/12/2023 12:03 PM
game page는 stateful이 더 효율적일수도 있다. 그런데 getX가 가진 기본적인 생각은 stateful이 비용이 너무 많이 든다. 상태관리를 하기 위해서 매번 dirty를 비교하지 말고 그냥 바로생성하는게 더 효율적이란 생각이다. 나도 getX의 주장이 맞다고 생각한다. 그래서 이것도 getX로 처리하는게 맞다. 그런데 stateful로도 작성 가능한 부분이긴 하다. 어떨때 stateless를 사용하고 어떨때 stateful을 사용하고, 어떨때 getX를 사용하는지 모른다는 의심이 든다.
[12:04 PM]
여튼 이것도 view이다. 근데 import한거 보면, business logic을 다 붙였다.
[12:04 PM]
스파게티 코드다.
[12:04 PM]
business logic을 다 걷어내야 한다.(refactoring)
[12:06 PM]
stateful을 한다고 mvc가 완전히 couple되었다고 생각하면 안되는게 stream과 같은 observer pattern을 사용하는 component들이 있기 때문에 코드 분리 작성이 가능하다.
[12:07 PM]
var users = GameController.to.users
.where((u) > u.id ! GameController.to.user.value.id);
[12:08 PM]
코드는 game방의 users정보를 가져와서 current user가 이 방에 있는지 check하는건데…
[12:09 PM]
여기서 궁금한건, 서버와 통신해서 데이터를 가져와서 객체를 만들었다면 그것을 어떻게 유지하는지가 궁금하다. 어떨때 또 통신하는지…이런것들은 메뉴얼이 있어야 도움이 된다.
hoyoul — 11/12/2023 12:11 PM k-holdem state class의 화면 [12:12 PM] kholdem이 보여주는 화면이 궁금했다. [12:13 PM] 여기도 우선 obx를 전체 page에 걸어놓으니…보기가 힘들다. (edited)

Figure 5: layout1
widget별로 나누어서 구현했다. [12:51 PM] ui는 뭐 어떻게 짜던 상관없지만, 이렇게 나눠서 짜는건 좋다. 근데 나라면 나누지 않을것이다. 재사용하지 않을것이기 때문에 오히려 page에 ui코드 다 넣는게 직관적일수 있다. (edited) [12:54 PM] 그런데 bettingBlocks widget과 hud widget을 나누는 이유는 getX를 잘모르기때문에 데이터처리에 힘들어서 이렇게 한거 같다. 근데 그러면 hud는 stateless로 짰어야 한다. 왜냐 ui가 변경되는 부분이 없기 때문이다. 따로 bettingblock으로 떼어 놨다면 그냥 그림인데….물론 이것은 GetX를 사용하지 않았을때의 가정이다. 여기서는 GetX를 잘못사용하고 있기때문에 언급할 필요는 없고, GetX를 사용하지 않고 짜도 이상한거란 얘기다. [12:55 PM] refactoring [12:55 PM] 다 해야한다. [12:56 PM] 내생각엔 간단한 데이터 처리하라고 하면 그냥 static함수 만들어서 가져다가 그냥 뿌리는 식으로 막짜는거 같다. [12:56 PM] 10분정도면 되는 간단한 코드 수정도 꼬여서 하루 이틀 걸릴꺼 같다.
hoyoul — 11/12/2023 12:57 PM 자연님도 자기가 만든 코드를 이해하고 있을까? 매번 코드를 봐야 이해될꺼 같다는 생각이 들었다. [12:58 PM] 여튼 대부분의 코드는 본거 같은데, 게임 flow는 모른다. 내가 섯다게임을 잘 모르기 때문에…이 게임을 어떻게 하는지도 모르겠고, 그거에 따른 통신 flow도 모른다. 백엔드와 프론트엔드간에 어떤식의 대화는 분명 있었을것이다. [1:01 PM] 그런데 이상한건…이런건…코드 리뷰를 했다면 다 리젝일텐데…코드리뷰는 아예 없는거 같다.
hoyoul — 11/12/2023 1:05 PM
GameController의 user에서 가져온다. 이건 user를 까보면 안다. [1:14 PM] 그러므로 구글로 로그인한 현재 사용자 계정으로 게임을 플레이하지 않는다. (edited) [1:17 PM] login할때, email정보를 서버에게 보냈던거 같다. 서버에서 다시 email에 해당하는 이름을 줬는지는 확인해야 한다. 이거는 debugging하면 된다. 그러면 받은 id가 있다면, 그걸 obx로 처리하고, 사용자를 선택할 필요가 없다. 그냥 text widget으로 해도된다. 그런데 중요한건 왜 dropbox로 선택하게 했냐? 라는 의문이 든다. 이것은 사용자가 여러 id를 가질수 있게 할 의도가 있는거다. [1:17 PM] 즉 A,B,C,D game player를 만들어서 자기가 선택해서 들어가게 만들고 싶었던 것이다. [1:18 PM] 그러면 google login해서 email주소와 인증 정보를 서버에 보냈을때, 서버는 해당 사용자가 가지고 있는 game name list를 frontend에게 전달해야 한다. 그래야 dropbox로 보여줄 수 있다. (edited) [1:18 PM] 이것도 디버깅하면 나온다. [1:19 PM] 구글 로그인한 사용자 계정으로 플레이 가능하도록 한다. (edited)
hoyoul — 11/12/2023 1:21 PM 현재는 google login 했을때 서버와 통신을 해서 player이름을 가져와서 보여줄수도 있고, 서버처리를 하지 않고 프론트엔드에서 johndoe user list를 뿌려주는 것일 수도 있다. [1:22 PM] 로긴이 되고 서버와 어떤 통신이 없다면, 그냥 email주소를 보여주는 식으로 할 수도 있다. [1:22 PM] 완료 기준 게임룸1로 입장하면 구글 사용자를 현재 사용자로 표시
다른 구글 사용자가 게임룸1로 입장하면, 각 사용자로 입장 표시
게임 시작 버튼 클릭후 게임 시작 [1:25 PM] 위에서도 말했듯이 서버로 부터 player id를 받는다면 보여주면 되고, 서버처리가 안되어 있다면 google login후에 gcp로 부터 받는 resource는 email밖에 없기 때문에 email을 보여주면된다. [1:25 PM] 다른 구글 사용자가 게임룸1로 입장하면, 각 사용자로 입장 표시 [1:27 PM] 이 부분이 좀 햇갈리는데…2card play라는 화면 자체가 게임룸1이라는 뜻으로 들리기 때문이다. [1:27 PM] 오늘은 여기까지만 하자. [1:28 PM] 이따 저녁때는 디버깅 환경을 확인하고, 내일부터 task에 따라 코드 고치고 전체적 refactoring은 상황봐서 하나씩 해가는 수 밖에 없다. November 13, 2023
hoyoul — 11/13/2023 8:32 AM ————————-[ 11.13 ]—————————- [8:33 AM] Kholdem 설계 from scratch (1) 개요 새로운 design에 따라서 게임을 만든다. 예전에 kholdem은 reference로 생각하고 새로 만들자. 어차피 design에서 중첩되는건 copy하고, 게임 통신도 분석한걸 기반으로 새로 구현하는게 더 낫다. UI부터 만들 것이다. (edited) [8:34 AM] (2) UI 설계 3개의 page를 stateless widget로 만든다. page이름: 가독성, 유지보수를 위해서 이름을 생각하자. (edited) [8:37 AM] 내가 생각한 이름들 1page-로그인화면: LoginScreen, AuthScreen, SignInScreen 정도
2page-게임선택화면, 게임방선택화면: GameSelectionScreen, GameLobbyScreen, GameRoomScreen 정도
3page-게임화면? GamePlayScreen, KholdemGameScreen…정도 (edited)
hoyoul — 11/13/2023 8:56 AM 결론 => LoginScreen, GameLobbyScreen, GamePlayScreen으로 하자. (edited) [8:57 AM] (2-1) LoginScreen (edited) [8:57 AM] 기존에 만든것과 비슷하다. 참조해서 만들면 될듯하다. (edited)

Figure 6: login screen
개발 [9:17 AM] kholdem_v1 이란 flutter project를 만든다. issue: kholdem-front app이름이 GCP에 등록되어 있다. 인증 과정에선 kholdem-front의 bundleID로 해야 토큰을 받아올 수 있을거라고 예상. 하지만, 이름 변경이 가능할듯하다. 참조: android: buildgraddle->applicationID변경, ios: ios/runner/info.plist-> CFBundleIdentifier를 수정. (edited)
hoyoul — 11/13/2023 9:31 AM github에 repo를 만든다. 이름을 잘못 만들었다. kholdem_front_v1으로 했어야 했다.
참고: .gitignore 처리 git으로 flutter를 만들때, gitignore가 필요하다. vscode로 project를 만들거나, android studio로 project를 만들면 자동으로 .gitignore가 만들어지는데, 이것은 많은 것이 빠져있다. ios나 android build시에 만들어지는 artifact를 거를수 없다. 컴파일러나 linker가 만들어내는 파일들이 일명 artifact라고 하는데 그것들은 git으로 추적, 관리 할 필요가 없다. 그래서 flutter 공식 github에서는 자신들이 만든 gitignore가 따로 있다. 이것을 사용하자. https://github.com/flutter/flutter/blob/master/.gitignore (edited)
flutter pub add get (edited) [9:56 AM] getX를 설치한다.
image를 위해서 asset에 /lib/images를 추가 PS: designer가 제공한 apple.png와 google.png는 해상도도 낮고, 크기가 다르다. program에서 확대 축소를 하면 이상해짐. 제공할때 동일한 크기와 quality있는 해상도의 이미지가 제공되면 좋음. lib/images 폴더를 만든다. pubspec.yaml에 assets: lib/images 추가

Figure 7: apple image
flutter pub add google_fonts Inter font를 사용하기 때문에 처리해줘야 한다. Inter는 Google font라서 flutter pub add google_fonts 를 하면 알아서 추가해준다. pubs.dev에 있는 package다. 이걸깔면 google font를 다 사용할 수 있다.
login screen만들때 생각해야 할것 (1) 기존에는 인증 여부를 check하는 코드가 제일먼저 실행됐다. 그 결과에 따라 login page와 gamelobby page를 선택한다. 어떤 의미에서 했는지 잘 모르겠다. 즉 login을 했다가 안사용하고 background로 있다가 다시 foreground로 왔을때의 처리인지? 그래서 우선 나는 login page가 보이게 할것이다. (edited)

Figure 8: apple image
같은 형태의 component가 사용될때는 코드가 중복된다. 그래서 component라는 폴더를 만들었다. 원래 만들 생각이 없었는데, 3page라서, 근데 game화면은 중복되는 component가 많기 때문에 만들기로 했다.
hoyoul — 11/13/2023 12:01 PM 결과

Figure 9: apple image
비슷하게는 만들었는데, 그냥 감으로 만들었다. 순상님이 준 android에서 기기별 디자인할때 참고라는 자료를 보고 좀 생각할께 있는지 확인해야겠다. => layout constraint는 java에서 swing에서 widget을 layout하는 방법이였다. 아마 kotlin에서도 layout설정시 하는 거 같은데, widget배치에 관한거라서…. 물론 swift나 지금하고 있는 flutter에서는 사용할 수 없고, device크기 변화에 따른 ui변경은 따로 찾아야 할듯하다. 궁금하다. 어떤식으로 되는지…지금 현재 섯다앱은 rotate에 대한 처리도 안되어 있어 보인다. 된건가? scroll방식은 아닌거 같은데…아예 landscape mode를 사용 안할런지도… (edited) [12:03 PM] 우선 만들었으니까, 내 local github에 push는 했다. (edited) [12:07 PM] (2-2) GameLobbyScreen

Figure 10: login2
기존에서 동그라미 어떻게 만드는지 보고, join 버튼은 social button 재사용하면 될듯한데, components/social_button을 rounded_button으로 바꾸는게 나을듯 하다. 아..옛날엔 화투패가 있었는데. 2장있던 그림. (edited)
hoyoul — 11/13/2023 4:54 PM 기존에는 listview와 stack view등 많은걸 가져다 썼다. 근데, 너무 복잡하다. 그냥 3개의 widget만 있으면 되지 않나? [4:58 PM] 기존에는 화투패그림이 있었다.

Figure 11: login4
11.14
ui를 만들때, 고민이 생겼다. 어떤 figma가 주어질때, 나는 어떤 생각을 해야 하는가? [8:32 AM] 경험으로 아 이럴때는 이렇게 하고 저럴때는 저렇게 하고, 그런 경험에 따른 작업방법? [8:32 AM] 물론 지금은 그렇게 한다. 이럴때는 이렇게 했었지…그런데 경험에 앞서서 어떤 철학이 필요하지 않을까?
hoyoul — 11/14/2023 8:35 AM 경험으로 한다는건 문제가 많다. 하나의 ui를 만드는데 100가지의 방법이 있을 수 있다. 그중 자신이 생각한, 아니면 경험한 그 방법이 옳다거나 효율적이라는 보장도 없다. 옳지 않거나 효율적이지 않아도 단지 익숙해져서 경험에 의해 작성하는건 아닌거 같다. [8:36 AM] 물론 경험으로 작성하려면, 어떤 철학이 있어야 한다. 나는 이런 생각으로 이렇게 작업했다. 라는 무언가가 있어야 한다. 그러면 비효율적이라도 수긍이가고 일리가 있다고 할만한 무언가… [8:37 AM] 단순히 이렇게 하면 이런 모양이 나와요…이건 너무 애들같다… [8:42 AM] 그럼 너는 ui를 어떤 생각으로 만드는데, 어떤 철학이 있냐?고 묻는다면…있긴 있다. 그런데…이게 확실히 말할수 없는게….내가 모르는 위젯이 많아서, 종합적으로 판단해서 한마디로 말할수 없는…즉 철학이 아니라는것이다. ui를 만들때, 중심을 꿰뚫는 생각이 있어야. 이게 ui의 핵심이야..그리고 이식대로 하면 돼. 나머지 세세한건 메뉴얼 보고 하면돼…이렇게 할 무언가가 있어야 한다. 근데 내 생각은 좀 붕떠 있다.
hoyoul — 11/14/2023 8:42 AM 내가 생각하는건, 무조건 단순하게 만들어야 한다. [8:42 AM] 근데 이건 철학이 아니라, 방법론이다. [8:45 AM] 예전에 qt나 swing같은…xwindow의 xwidget을 다룰때도 비슷했지만, 그때는 design을 하는 방법론?을 manual로 제공해서 그 식대로 작성하기 때문에 지금과는 좀 다르다. [8:46 AM] 미켈란젤로가 이렇게 말했었다. 조각은 쉽다. 왜냐, 대리석안에 조각은 있고, 내가 했던건 조각주변의 돌덩이를 제거하는게 전부니까… [8:48 AM] 근데, flutter의 ui는 반대다. 조각에 해당하는 content들은 날라다닌다. 나는 이런 조각들을 돌덩이로 채운다. 예를들어, center를 통해서 조각을 중심으로 모으고, 이 중심에서 sizedbox같은 돌덩이로 조각을 고정시킨다.
hoyoul — 11/14/2023 8:51 AM 화면이라는 대리석을 만드는것이다. sizedbox나 container같은 돌조각들을 사용해서, 공중에 날라다니는 text나 이미지같은 content라는 조각들을 고정시킨다. [8:52 AM] 그래서 내가 만든 ui는 center를 사용해서 중앙에 중력을 만든다. 날라가는 content를 붙잡기 위해서… [8:53 AM] 그리고 sizedbox같은 돌덩이로 content들을 고정시키는 작업을 한다. 미켈란젤로와 정반대의 일을 한다. [8:54 AM] 누가 나한테 ui를 처음작성하는데 감이 안와요. 라고 묻는다면 이렇게 답할건데…이게 문제가…내가 안다뤄본 위젯이 너무 많다는것이다. 100개중에 지금 5개정도 사용해봤는데…. [8:56 AM] 똑같은 화면을 작성하는데, 자연님은 한 15개에서 20개의 위젯을 사용했였다. stack, positioned…expanded…등등…처음보는것도 많이 썼다. [8:57 AM] 나는 5개정도로 비슷하게 만들었다. 그런데…내가 이렇게 만드는게 맞느냐?라고 묻는다면 확신이 안선다. 그냥 경험으로 만든것이다. 그리고 이렇게 만들어도 되냐? 라는 의심이 든다…경험의 문제일까? 지식의 문제일까? siver bullet이 여기에도 있는것일까?
hoyoul — 11/14/2023 9:00 AM 커피 한잔 먹고 일하자.
hoyoul — 11/14/2023 9:10 AM 생각나는 모든것을 적고, 저녁업무때, 잡담같은것들은 지울것이다. 왜냐 찾을때 너무 불편하다. [9:10 AM] (2-3) GamePlayScreen
hoyoul — 11/14/2023 9:17 AM

Figure 12: card2
여기서 재사용 가능한 component를 생각해보자. [9:19 AM] 카드, command(action button), money label..정도 보인다. [9:21 AM] 아..그전에 어제 작업한거 git에 올려야 한다.
hoyoul — 11/14/2023 9:46 AM 재사용 가능한 component는 oop로 말하면 class다. UI programming이 아니라면, Card라는 class를 만들었을것이다. 그리고 action들도 class로 처리했을것이다. 그런데 widget도 class이기 때문에 widget으로 만들면 된다. 그리고 그 widget이 가진 data들을 GetXcontroller로 빼내야 한다. [9:47 AM] card라는 class를 만든다고 해보자.
hoyoul — 11/14/2023 9:54 AM class Card{ String imgPath;//앞면, 뒷면 imgpath도 처리해야 할듯. String name; Card(this.imgPath, this.name); } [9:54 AM] 이렇게 class를 만드는 대신에 stateful을 사용하는 경우에는 다음과 같이 할 것이다. [9:56 AM] class Card extend StatefulWidget{ String imgPath; String name; createState() } class CardState extend State{
} [9:57 AM] GetX를 사용한다면 다음과 같이 할것이다. class CardWidget extend StatelessWidget{ final cardController = Get.find(); } [9:59 AM] 그런데, 나는 Getx에서 Controller를 따로 decouple해서 이름을 controller를 prefix하는건 별루다. 왜냐면 controller에는 data가 있기 때문에 model이름을 하는게 더 낫지 않을까 하는 생각이다. 그래서 위에서 class Card는 model로 하고 GetxController 를 상속하는식으로 처리하는게 좀 맞지 않나? 하는 생각이 든다.
hoyoul — 11/14/2023 10:09 AM 우선 ui를 그냥 작성하기로 한다. 왜냐면, model과 controller 그리고 http통신은 생각해야 할께 있기 때문에 나중에 수정을 해야한다. 즉 ui에서 data를 뽑아내고, 그 data중에서 상태 데이터를 선별하고, 또 어떤 데이터는 서버로부터 가져오고, 그것에 맞춰서 class 를 만들어야 한다. class diagram으로 명확하게 볼수 있게 해야한다.
hoyoul — 11/14/2023 11:02 AM 읽어볼만한 article이 있다. [11:02 AM] https://medium.com/@ximya/creating-the-ultimate-base-screen-class-based-on-mvvm-architecture-in-flutter-getx-part-1-58dada8e4266
component를 만들때 data는 외부에서 생성자로 받는 식으로 해야 한다. 왜냐면 내부의 데이터를 사용하는 방식으로 하면, business logic이 component에도 들어가게 된다. 내부에서 state data를 처리못하고 한번 사용하고 버리는 stateless로 짜야 한다. 오직 state data는 외부에서 제공하게 해야 한다.
hoyoul — 11/14/2023 11:27 AM money_label component
hoyoul — 11/14/2023 4:35 PM project에서 관리하는 폴더에 postfix를 붙인다. pages: screen, components: widget November 15, 2023
11.15
card_widget component
hoyoul — 11/15/2023 9:09 AM container로 카드를 나타낼려고 한다. (edited) [9:13 AM] container widget [9:13 AM] https://api.flutter.dev/flutter/widgets/Container-class.html
[공식문서 내용] A convenience widget that combines common painting, positioning, and sizing widgets. (edited) [9:16 AM] 편리한 위젯인데….뭐하는데 편리한? combine하는데, combine한다는건 합친다는건데….
hoyoul — 11/15/2023 9:17 AM widget들을 합치게하는데 편리하다는데…좀 어렵다. common painting, common positioning, common sizing은 어떤 의미일까? [9:17 AM] 더 읽어봐야 할듯 하다. [9:18 AM] [ 공식 문서 내용] A container first surrounds the child with padding (inflated by any borders present in the decoration) and then applies additional constraints to the padded extent (incorporating the width and height as constraints, if either is non-null). The container is then surrounded by additional empty space described from the margin. (edited) [9:21 AM] html의 css 박스 모델을 설명을 듣는듯하다. 근데 처음 문장에서 first가 의미심장하다. 어떤 과정을 설명하는듯하다. (edited)
hoyoul — 11/15/2023 9:24 AM container는 child를 갖는다. container와 child사이에는 padding이 있고, padding은 inflate(부풀어오르는데)되는데, 최대 container가 가진 border까지 부풀어 오를수 있다. 그리고, container밖에는 margin으로 둘러싸여져있다. html tag의 css속성을 설명하는 것과 같다. 즉 첫번째 하는일은 child를 padding으로 둘러싸는데, padding에 constraint가 설정되어 있다면 적용하고, 그 다음 margin으로 둘러 싸여진다는 건데, 예를들어서 container( child: Text(’test’), ) (edited)
hoyoul — 11/15/2023 9:51 AM 이 경우, 첫번째로 하는 것은 Text를 padding으로 둘러싼다. padding에 대한 설정이 없기 때문에 적용할 constraint는 없다. 그리고 margin도 정의되어 있지 않으니까 margin공간도 없다. 그러면 어떻게 되는가? container의 size는 child인 text와 같다. 즉 container의 appearance는 없다. child인 Text만 보일 것이다. container의 size는 child인 Text와 같다. (edited) [9:58 AM] 위 문장에서 우리가 놓쳐선 안되는게 있다. container의 속성
- child, margin, padding, decoration(border)
hoyoul — 11/15/2023 10:00 AM 첫번째 문단에서 제공하는 설명은 신경쓰지 않고 그냥 넘어갈수도 있는데, 주의 깊게 봐야 하는건, container는 액체괴물과 같아서, 위 4가지의 속성이 적용이 되서 appearance를 갖게 된다는 의미가 있다는 것이다.
hoyoul — 11/15/2023 10:09 AM 다른 예를 들어보자. [10:11 AM] container( child: Text(’test’), padding: EdgeInsetsGeometry.all(20), )
hoyoul — 11/15/2023 10:17 AM container에서 padding 속성이 특이하다. 속성의 값이 Padding객체가 아니다. EdgeInsetsGeometry라는 abstract class를 사용한다. 그리고 all 함수의 return값으로 value로 사용한다. flutter에서 widget 생성자에 기술되는 속성은 기억하기 쉽게 동일한 이름으로 정한다고 들었다. 예를 들면, padding: Padding(), 이런식으로 designed되었다고 문서에서 읽은적이 있다. 그런데, padding의 값으로 Padding객체가 아닌, EdgeInsetsGeometry의 static method를 사용한다. 예를 들어 EdgeInsetsGeometry.all()와 같이…이것은 factory pattern처럼 padding 객체를 return하는건 아닐까? (edited) [10:19 AM] 참고로 EdgeInsetsGeometry는 abstract class다. abstract class는 method중 하나만 미구현되도 abstract class다. 저기서 all이란 method는 물론 static method로 위에서 말한 Padding instance는 아니고, EdgeInsets라는 class의 instance를 return한다고 한다. (edited) [10:19 AM] EdgeInsetsGeometry라는 이름. (edited) [10:21 AM] Edge는 가장자리를 뜻하는건 알겠다. insets, outsets라는 단어는 widget을 설계할때 사용이 되는것을 본적이 있다. 즉 내부공간을 뜻하고, 또 Geometry는 rendering할때 문서에 많이 나오는 단어인데, 그냥 기하학으로 해석하는게 아니라, 내부 공간을 계산하는 모든 방법을 퉁쳐서 geometry라고 한다. (edited) [10:24 AM] 즉 EdgeInsetsGeometry는 내부공간을 계산하고 처리하는 abstract class고, Padding class나 Margin class의 상속트리를 찾아보면 이것을 상속받아서 사용한다. 하지만 container에서 여백을 나타내는 instance는 Padding, Margin 대신에 별도의 EdgeInsets라는 EdgetInsetsGeometry를 상속받은 class의 instance를 사용한다. EdgeInset객체가 여백을 처리한다고 보면된다. 그런데, Margin이 궁금하다. Margin은 inset이 아니라 outset 공간을 처리하지 않나 ? 그렇다면 outsets를 계산하는 EdgeOutsetsGeometry를 상속 받아야하는거 아닌가? (edited)
hoyoul — 11/15/2023 10:27 AM 그런데 EdgeOutsetsGeometry라는 클래스는 없다. 왜냐? margin도 container입장에선 insets에 해당한다. 다음 그림을 보자. (edited)

Figure 13: container1
container의 입장에선 margin도 inset, padding도 inset이다. border의 입장에선 margin은 outset이 될지 몰라도 container 입장에선 아니다. container가 아닌, 다른 widget도 모두 여백은 inset이다. 그래서 flutter에서는 여백을 나타내는 클래스를 EdgetInsetsGeometry로 만들었다. 실제적인 여백은 EdgeInsets instance다. 참고로 파생 클래스를 보면 다음과 같다. (edited) [10:42 AM]

Figure 14: container2
flutter에서 여백 [10:45 AM] 여백을 정의하자면, flutter에서 margin과 padding이라는 용어로 사용되며, 그것은 위와 같은 class로 구현되어 있다. 특징이 있는데, size와 direction이 있다는 것이다. [10:46 AM] size는 width, height로 나타내고 direction은 4방향을 가지고 있음을 위 그림에서도 볼 수 있다. 그리고 여백을 concrete하게 나타내는 instance는 EdgeInsets 객체다. (edited)
hoyoul — 11/15/2023 11:04 AM 공식 문서를 이어서 보자. (edited) [11:04 AM] [공식문서 내용] During painting, the container first applies the given transform, then paints the decoration to fill the padded extent, then it paints the child, and finally paints the foregroundDecoration, also filling the padded extent. [11:08 AM] widget들은 모두 각각의 paint()를 가지고 있다. rendering하기 위해서 build()를 호출하면 최종적으로 각각의 widget이 가진 paint()가 호출될꺼라고 생각된다. 왜냐면 swing에서도 그렇고 다른 gui system도 그렇기 때문이다. window, linux,mac과같이 gui os들은 모두 widget을 사용하고, widget들은 paint()가 있고, repaint()도 제공되기 때문이다. [11:10 AM] container의 paint()가 호출될때 하는 일을 적은것인데, 이건 animation을 하거나, paint()를 직접 호출하거나, customize가 필요할 때는 필요하지만, container를 사용만 한다면 몰라도 사용할 수 없다. 그래서 pass한다.
hoyoul — 11/15/2023 11:11 AM [공식문서 내용] [11:11 AM] Containers with no children try to be as big as possible unless the incoming constraints are unbounded, in which case they try to be as small as possible. Containers with children size themselves to their children. The width, height, and constraints arguments to the constructor override this.
hoyoul — 11/15/2023 11:18 AM 이게 어렵다. container가 자식이 있는경우는 자식의 크기에 맞춰진다고 했다. [11:18 AM] 자식이 없으면 어떻게 될까? [11:22 AM] container를 구성하는 요소인 margin, padding, child, decoration은 필수가 아니였다. 그래서 child가 없다면? 그 크기는 어떻게 될까? 인데, 우선 문장해석이 좀 까다롭다. [11:23 AM] 자식이 없는 container중, constraints가 있는것과 없는 것에 대한 설명이다. 예를 들어, width, height가 지정되어 있다면 가능한한 big해질려고 한다고 한다. [11:23 AM] container( with:200, height:200, color: Colors.red, ) [11:24 AM] 이 경우 자식이 없지만 constraint가 있다. 즉 최대한 200x200크기로 될려고 한다고 한다. 그런데 width와 height가 없다면 가능한 0x0이 될려고 한다.라고 적혀있다.
hoyoul — 11/15/2023 11:26 AM 가능하면 커질려고 한다. 가능하면 작아질려고 한다.라는 표현 [11:28 AM] 좀 모호하다. 가능하다? 즉 다른것에 영향을 받기 때문이 아닐까? 예를 들어보자. Row( children[ container( width: 700, height:100, ), container( ) ] ); [11:29 AM] 만일 screen의 width가 800이라고 하자. 그러면 첫번째 container는 가능하면 700에 맞출려고 한다. 두번째 container는 가능하면 0에 맞출려고 한다. 그래서 문제가 없다. 그런데 다음 예를 보자. [11:30 AM] Row( children[ container( width: 700, height:100, ), container( width: 200, height:100, ) ] ); [11:31 AM] 이 경우는 둘다 가능한 700을 맞출려고 하고 200을 맞출려고 한다. 그런데 screen size가 800이면 둘은 적절하게 합의해서 크기를 조절하게 된다는 뜻이다.
hoyoul — 11/15/2023 11:52 AM [ 공식문서 내용] [11:52 AM] By default, containers return false for all hit tests. If the color property is specified, the hit testing is handled by ColoredBox, which always returns true. If the decoration or foregroundDecoration properties are specified, hit testing is handled by Decoration.hitTest. [11:54 AM] hit test가 뭔지 모르겠다. 찾아보니, 우리가 touch하는게 hit라고 한다. 그리고 touch했을때 좌표가 widget에 전달되어 contain여부를 확인하고 이벤트 처리를 한다고 한다. 즉 hit되었을때 이게 widget내의 영역인지를 test하는것을 hit test라고 한다. [11:55 AM] 문서대로 해석하면 될듯하다. pass.
hoyoul — 11/15/2023 12:02 PM [공식문서 내용] [12:02 PM] Layout behavior See BoxConstraints for an introduction to box layout models.
Since Container combines a number of other widgets each with their own layout behavior, Container’s layout behavior is somewhat complicated.
Summary: Container tries, in order: to honor alignment, to size itself to the child, to honor the width, height, and constraints, to expand to fit the parent, to be as small as possible. [12:04 PM] box layout은 css box layout을 생각하면 될듯하다. layout이 좀 복잡하다고 한다. [12:07 PM] container는 aligment를 honor한다? alignment를 준수하려고 한다? 뭔소리지, size는 child에 맞추고, width height같은 제약을 준수하는데, 가능하면 부모의 fit에 맞춰 확장되거나, 가능하면 작아지려고 한다? 정확히 모르겠다. [12:07 PM] More specifically:
If the widget has no child, no height, no width, no constraints, and the parent provides unbounded constraints, then Container tries to size as small as possible.
If the widget has no child and no alignment, but a height, width, or constraints are provided, then the Container tries to be as small as possible given the combination of those constraints and the parent’s constraints.
If the widget has no child, no height, no width, no constraints, and no alignment, but the parent provides bounded constraints, then Container expands to fit the constraints provided by the parent.
If the widget has an alignment, and the parent provides unbounded constraints, then the Container tries to size itself around the child.
If the widget has an alignment, and the parent provides bounded constraints, then the Container tries to expand to fit the parent, and then positions the child within itself as per the alignment.
Otherwise, the widget has a child but no height, no width, no constraints, and no alignment, and the Container passes the constraints from the parent to the child and sizes itself to match the child.
The margin and padding properties also affect the layout, as described in the documentation for those properties. (Their effects merely augment the rules described above.) The decoration can implicitly increase the padding (e.g. borders in a BoxDecoration contribute to the padding); see Decoration.padding.
hoyoul — 11/15/2023 12:12 PM 고려되는게 부모, 자식, 그리고 constraints와 alignment인데…좀 복잡하다. 부모는 container를 둘러싼게 부모로 볼수 있겠고, 예를 들면, Row( children: [ container(), ]) [12:14 PM] alignment는 뭐지? alignment라는 속성이 있다. 자식의 위치를 정해주는거라고 한다. 예를 들면, container( child: Text(’test’), alignment: Alignment.center, ) [12:19 PM] 각각의 경우를 생각해보자. (1) 자식도 없고, constraints도 없고, 부모도 constraint도 없다면, container는 가능한 작아질려고 한다. Rows( children: [ container() ], ) [12:19 PM] 0x0크기의 container가 만들어질듯…
hoyoul — 11/15/2023 12:37 PM (2) 자식도 없고, alignment도 없다. 하지만 constraint가 있다. => 최대한 constraint에 맞출려고 한다. 부모에 대한 언급은 없다. Rows( children: [ container( width: 200, height: 200, )]) [12:37 PM] 200x200 container가 만들어질듯… [12:38 PM] (3) 자식도 없고, alignment도 없다. constraint도 없다. 부모에 constraint가 있다. => 부모의 fit에 맞출려고 한다. Rows( width: 200, height: 200, children: [ container(),] ) [12:39 PM] 부모의 size 200x200에 맞출듯… [12:43 PM] (4) 자식이 있고, alignment가 있다. constraint도 없고 부모의 constraint도 없다. => 자식에 맞춘다.(around하게…최대한 가깝게) [12:44 PM] Rows( children: [ container( alignment: Alignment.center, child: Text(’test’), )]) (edited)
hoyoul — 11/15/2023 12:44 PM alignment가 의미가 있는지 모르겠다. 자식이 여러개 있다면 의미가 있을듯… [12:46 PM] (5) 자식이 있고, alignment가 있다. constraint는 없다. 부모의 constraint는 있다. => container는 부모의 fit에 맞출려고 한다. 자식은 alignment에 따라 배치된다. [12:46 PM] Rows( width: 200, height: 200, children: [ alignment: Alignment.center, container( child: Text(’test’), )]) (edited) [12:47 PM] 200x200크기의 container가 만들어진다. [12:47 PM] 여튼 해석에 어려움이 없다. pass [12:51 PM] 예제 2개만 보겠다.
hoyoul — 11/15/2023 12:52 PM 배달왔다. 밥먹고…다시…

Figure 15: center1
Center( child: Container( margin: const EdgeInsets.all(10.0), color: Colors.amber[600], width: 48.0, height: 48.0, ), ) [1:51 PM] 간단한 예제다. 그런데, 이해가 안간다. 왜냐면 48x48의 container인데 margin이 4방향으로 10이 있다고 하더라도, amber색 영역은 38x38이 아닌거 같기 때문이다.

Figure 16: center2
예제가 잘 못됐다. padding이 기술이 안되면 padding의 size는 0이다. [2:14 PM] 또 다른 예제를 보자.

Figure 17: center3
Container( constraints: BoxConstraints.expand( height: Theme.of(context).textTheme.headlineMedium!.fontSize! * 1.1 + 200.0, ), padding: const EdgeInsets.all(8.0), color: Colors.blue[600], alignment: Alignment.center, transform: Matrix4.rotationZ(0.1), child: Text(‘Hello World’, style: Theme.of(context) .textTheme .headlineMedium! .copyWith(color: Colors.white)), ) [2:16 PM] 꽤 복잡하다. [2:19 PM] padding, color, alignment, child는 배운거라서 알수 있다. 그런데 constraints, transform은 설명이 없었다. painting하는 과정에서 transform을 수행한다는 얘기는 있었지만, 예시가 없었다. 그리고 constraints는 width, height만 봤는데, constraints라는 속성이 별도로 있다.
hoyoul — 11/15/2023 2:30 PM constraints 속성 (edited) [2:33 PM] container의 constraints에 해당하는 것은 width와 height이다. 그런데 이런 width와 height를 확장해서 좀 더 세밀하게 제어하기 위해서 constraints속성 필드가 있다고 한다. 보통 BoxConstraints를 사용하는데, 여기에는 maxWidth, minWidth, maxHeight, minHeight가 사용되는데, 좀더 expand해서 위와 같이 테마의 헤드라인 글꼴크기 값을 계산해서 크기를 정할수도 있다고 한다. 그런데 생각해보면 그냥 constraints 속성없이 height: ThemeOf blah blah….해주면 똑같지 않나? 라는 생각이 든다. [2:36 PM] ps: height로 지정하면 부모나 자식의 영향을 받는데, constraints에서 height로 설정하면, 영향을 받지 않고, 최대한 constraints의 height 크기에 맞춰진다고 한다.
hoyoul — 11/15/2023 2:37 PM transform 속성 [2:39 PM] transform은 변형인데, 변형은 scale, translation, rotate등등이 있다고 한다. Matrix4라는 matrix를 이용한다. 원래 graphics는 linear algebra에 영향을 받았는데, matrix가 transform에 사용된다. 필요할때 사용하면 된다. [2:42 PM] Card widget (Continued…)

Figure 18: card3
card는 앞면과 뒷면이 있다. 근데 뒷면이 두가지가 있는게 차이가 있는가? [3:44 PM] 차이가 있다면 3가지 상태로 나타내야 한다.
hoyoul — 11/15/2023 3:56 PM 카드의 앞면 12개와 뒷면도 이미지로 만들자. enum으로 할수도 있지만, class로 구현하면 enum을 사용할 필요는 없다. 왜냐? 객체에서 숫자값을 가지고 있기 때문에 enum을 사용하지 않아도 된다. 물론 enum으로 문자열로 값을 간단히 처리할 순 있지만, 뭐 상황에따라…. [3:57 PM] 그래서 앞면 뒷면 상태를 나타내는 변수를 만들던가, bool값으로 앞면이면 true, 아니면 false로 하면 된다.
hoyoul — 11/15/2023 4:07 PM 작은 카드는 48x70 크기고, 큰 카드는 60x90크기다. [4:08 PM] size정보를 외부에서 주고, 카드정보도 외부에서 주자.
hoyoul — 11/15/2023 4:37 PM 어떤 정보가 필요할까? class CardWidget extends StatelessWidget { final int cardNumber; final String frontImagePath; final String backImagePath; final double width; final double height; final bool isPlayerCard; [4:37 PM] 이런식으로 외부에서 받는건 아닌거 같다. [4:39 PM] 고려해야 할께, player카드인지, other player카드인지를 생각하자. 왜냐 player card는 크기가 크다. 60x90이고 다른 player는 48x70이다. 따라서 bool isPlayerCard를 인자로 전달받으면 card size를 별도로 받을 필요는 없다. [4:40 PM] 뒷면 카드를 별도로 유지할 필요는 없다. card number를 0이면 뒷면이라고 하자. [4:41 PM] 그러면 입력으로 cardnumber와 isPlayerCard만 입력받으면 image와 size를 내부에서 처리하면된다.
hoyoul — 11/15/2023 4:45 PM 따라서 아래처럼 하면 어떨까? [4:46 PM] class CardWidget extends StatelessWidget { final int cardNumber; final bool isPlayerCard;
const CardWidget({ Key? key, required this.cardNumber, required this.isPlayerCard, }) : super(key: key);
hoyoul — 11/15/2023 5:02 PM 그런데 어차피 내부에서 처리하는 business logic은 다 getxController로 보낼거라서…예를들면 map을 사용해서 card number와 card image path를 mapping하는거라던지, card type으로 size를 정하는것들은 GetxController를 상속한 모델에서 처리할 것이다. 그래도 보이기 위해선 widget에서 처리하는 코드를 만들긴 해야 한다.
hoyoul — 11/15/2023 6:57 PM 간단하게 isPlayCard가 true이면 60x90의 size를 가져야 하니까… double width = isPlayerCard ? 60.0 : 48.0; double height = isPlayerCard ? 90.0 : 70.0; [6:59 PM] cardNumber에 따른 imagePath는 cardNumber를 입력받으면 switch case로 일치하는 path를 return하는 함수를 만들어도 되고, map을 써도 된다. [6:59 PM] 근데 요즘은 추세가 switch case나 if문 써서 이런 처리를 하지 않는다. map을 쓰기로 한다. [7:03 PM] 대충 작성하자면, String _getImagePath(int cardNumber) { / map은 외부로 빼내도 되는데, 그냥 local로 만들어서 쓰자. final Map<int, String> cardImages = { 1: ’lib/images/cards/card_1.png’, 2: ’lib/images/cards/card_2.png’, / 나머지 카드 쭉 쓴다. 12: ’lib/images/cards/card_12.png’, };
return cardImages[cardNumber] ?? ’lib/images/cards/back.png’; } (edited)
hoyoul — 11/15/2023 7:05 PM 1-12까지의 수를 넣으면 card의 image path를 return하고 그 외의 값은 뒷편을 나오게 하는것이다. [7:05 PM] 현재 slot은 표시 하지 않게 하겠지만, slot을 표시해도 코드변환은 크지 않을것이다. 왜냐 slot은 그냥 view의 모양만 바꿀뿐 데이터를 바꾸지 않기 때문에 큰 상관없다.
hoyoul — 11/15/2023 7:51 PM 근데 저렇게 1-12까지 동일한 string을 계속 반복해서 쓰는건 아닌거 같다. 근데 지금 떠오르는 생각 이없다. string interpolation을 사용하면 간단하게 표시할 수 있을거 같은데… [7:51 PM] 그냥 우선은 pass하자..저게 직관적이긴 하다.

Figure 19: card5
money component와 card component가 정상 동작하는지 확인만 했다. [8:43 PM] 뒷면에는 slot없는걸로 테스트했다. 물론 나중에 슬롯을 추가하더라도 view에만 적용되기 때문에 문제없다. [8:47 PM] player마다 card 놓여진다. 4명이서 하기 때문에 중복이 된다. component로 만들어야 한다. 어떻게 naming을 할까?
hoyoul — 11/15/2023 8:49 PM 놓여지는 장소니까 CardPlace, CardArea, CardPanel, CardDisply, CardsInHands… [8:49 PM] PlayerCardPlace?
hoyoul — 11/15/2023 8:58 PM 근데, 만들어야할 component가 좀 더 있다.

Figure 20: card4
각각의 component는 중복이 되고, 그리고 여러번 사용되니까 component로 만들어 재사용한다. [8:59 PM] component에는 끝에 widget이란 postfix를 붙인다. 예를들어, Card는 CardWidget이다. [9:01 PM] compoent를 다 만들면, page에 붙이면 된다. [9:01 PM] 아무래도 내일해야 할듯하다.
hoyoul — 11/15/2023 9:11 PM playerpot component는 PlayerBet component가 낫겠다. 저기서 betting이 뭐고 자기돈이 무엇인지 알아야 한다. 그거에 맞게 네이밍을 하자.
hoyoul — 11/15/2023 10:43 PM stack widget [10:43 PM] https://api.flutter.dev/flutter/widgets/Stack-class.html Stack class - widgets library - Dart API API docs for the Stack class from the widgets library, for the Dart programming language. [10:43 PM] A widget that positions its children relative to the edges of its box.
This class is useful if you want to overlap several children in a simple way, for example having some text and an image, overlaid with a gradient and a button attached to the bottom.
hoyoul — 11/15/2023 10:52 PM stack은 자식들을 위치시키는 widget이라고 한다. layout관련된 widget임을 알수 있다. 그런데 relative to the edges of its box라고 한다. widget이 box형태로 되어 있고, 박스안에서 children을 배치하는 기준이 절대위치가 아닌 박스 모서리의 상대 위치라고 하는거 같다. 즉, 상대 좌표를 사용하는 거 같다. [10:53 PM] 그 다음 문장이 어렵다. [10:57 PM] children을 겹치게 하고싶다면, 간단히 할수 있다고 한다. 예를 드는데, text, image를 gradient와 overlaid하게 하고 아래는 버튼을 붙이는경우가 있다는데….우선 overlaid가 뭔지 모르겠고, gradient라는 widget이 있나? 우선 넘어가 보자. [10:58 PM] A overlaid with B: A가 B위에 덮여져 있다. 즉 위에 A가 있고 아래에 B가 있다고 한다. [10:59 PM] 즉 text와 image가 gradient와 아래 button위에 덮여진경우를 보자. 는 말인데…그래도 어렵다.
hoyoul — 11/15/2023 11:00 PM Each child of a Stack widget is either positioned or non-positioned. Positioned children are those wrapped in a Positioned widget that has at least one non-null property. The stack sizes itself to contain all the non-positioned children, which are positioned according to alignment (which defaults to the top-left corner in left-to-right environments and the top-right corner in right-to-left environments). The positioned children are then placed relative to the stack according to their top, right, bottom, and left properties. [11:06 PM] stack에 children은 두 종류가 있다고 한다. positioned children과 non-positioned children. positioned children은 positioned widget으로 둘러싸이면 positioned children이라고 한다. positioned widget은 그럼 뭐지? 여튼, 그런데, 그냥 positioned widget도 아니다. non-null 속성을 적어도 한개 가진 positioned widget으로 둘러싸여야지만, positioned children이 된다고 한다.
hoyoul — 11/15/2023 11:08 PM non-null속성은 또 뭐지…그럼 어떤 속성들이 있는지 먼저 설명해 주어야 하는거 아닌가?
hoyoul — 11/15/2023 11:16 PM stack size 그자체가 모든 non-positioned children을 포함할거라고 한다. 우선 positioned children과 non-positioned children을 내가 이해를 못하고 있다. [11:19 PM] 아! 동영상을 먼저 봐야 한다. 동영상에서 보면 positioned widget은 widget의 이름이다. Positioned라는 위젯이 있다. pp형태로 되어서 햇갈렸다. 위에서도 대문자로 Positioned Widget이라고 되어 있다. [11:19 PM] Positioned Widget [11:20 PM] Positioned Widget을 보니까, 속성값으로 botton,left,right,top, child와 같은 속성이 있다.
hoyoul — 11/15/2023 11:41 PM https://api.flutter.dev/flutter/widgets/Positioned-class.html Positioned class - widgets library - Dart API API docs for the Positioned class from the widgets library, for the Dart programming language. [11:42 PM] Positioned Widget을 먼저 이해한 후에 Stack Widget을 봐야할 듯 하다. [11:42 PM] A widget that controls where a child of a Stack is positioned.
A Positioned widget must be a descendant of a Stack, and the path from the Positioned widget to its enclosing Stack must contain only StatelessWidgets or StatefulWidgets (not other kinds of widgets, like RenderObjectWidgets). [11:45 PM] 일반적으로 stack은 2차원 공간상에서 볼수 있고, 3차원 공간상에서도 생각할 수 있다. Stack이 가진 기본 전제는 children이 있다는 것이다. children중 어떤 child의 stack안에서의 위치를 정할수 있게 해주는 widget이란 거 같다.
hoyoul — 11/15/2023 11:50 PM Positioned widget이 Stack의 후손? 이여야 한다. 후손이라는건, 자식, 손자, 증손자를 포함하는건데 뜻하는건데…stack의 자식은 Stack( children: [ Text(’test’), ]) (edited) [11:50 PM] Text가 Stack의 자식이다. [11:52 PM] 그러면 Positioned Widget이 Stack의 자식이 되는건, Stack( children: [ Positioned(), ]) [11:53 PM] Positioned Widget이 Stack의 손자가 되는건, Stack( children: [ Row( children: [ Positioned(), ])]) [11:54 PM] 즉, Positioned Widget은 stack과 떨어져서 생각할 수 없다. 무조건 자식이던, 손자가 되던 해야한다. 뭐 이런뜻… [11:56 PM] enclosing stack이란 단어는 무슨뜻이지? en은 enable이 생각나고 closing은 닫다..뭐 이건데…en이란게 prefix로 어떤 가능성을 말하는건데…그러면 enclosing이 뭐지? November 16, 2023
hoyoul — 11/16/2023 12:04 AM enclose가 넣다, 에워싸다, 동봉하다.의 뜻이라고 한다. 아 맞다. 봉투에 넣을때 enclose라는 표현을 썼던거 같다. 둘러싸다 인데…
hoyoul — 11/16/2023 12:19 AM 그러면 Positioned widget으로 부터 자신을 둘러싼 Stack까지의 path상에는 stateless나 stateful widget들만 허용된다. RenderObjectWidget은 허용되지 않는다. 즉 위에 내가 든 손자의 예를 보면 positioned widget과 stack path사이에 Row가 있다. Row는 stateless?이기 때문에 괜찮다. 그런데 RenderObject Widget은 안된다. 뭐 이런소린데…RenderObjectWidget은 뭐지? 이거 widget tree에서 본거 같은데… [12:20 AM] If a widget is wrapped in a Positioned, then it is a positioned widget in its Stack. If the top property is non-null, the top edge of this child will be positioned top layout units from the top of the stack widget. The right, bottom, and left properties work analogously. [12:21 AM] 이건 해석이 어렵지 않은데 바로 앞에 동영상이 있다. 한번 보자. [12:24 AM] 동영상에선 쉽게 positioned widget을 설명한다. stack안에 어디 위치할 지 몰라서 돌아다니는 widget이 보이고, 그 위치를 지정해주는 속성을 Positioned의 인자로 지정해준다. flutter의 logo로 보여주는데, 어렵지 않다.
hoyoul — 11/16/2023 12:28 AM 다시 해석을 해보면, Positioned Widget으로 둘러싸인 widget, 아마도 positioned widget의 child를 얘기하는듯하다. Positioned 위젯의 자식은 stack내에서 위치시키는것에 대한 예로, top속성이 정해져 있으면, stack에서 그만큼 떨어져서 위치해진다. 뭐 이 정도로 이해하면 될듯하다. Positioned에 top,left,right,bottom이라는 속성이 있기 때문에…상대적 위치를 기술한다. [12:28 AM] If both the top and bottom properties are non-null, then the child will be forced to have exactly the height required to satisfy both constraints. Similarly, setting the right and left properties to non-null values will force the child to have a particular width. Alternatively the width and height properties can be used to give the dimensions, with one corresponding position property (e.g. top and height).
hoyoul — 11/16/2023 12:43 AM Positioned위젯에 top,bottom속성이 있다면 강제적으로 child의 height가 결정되고, left와 right가 있다면 child는 강제적으로 width가 결정된다고 한다. 그리고 alternatively가 해석하기가 좀 tricky한데, 다른 방법이 있다. 강제적으로 child의 width와 height를 정해주지 않고, 다른 방법을 사용할 수도 있다고 한다. 그것은 width와 height라는 속성이 child의 dimension을 제공하는데 사용할 수 있다고한다. 근데 width와 height가 이미 dimension정보 아닌가? 그런데 일치하는 position속성과 함께 제공될때, dimension을 제공할 수 있다..좀 이상하다. 예를 들어서 top이라는 position속성과 height라는 정보가 주어지면 dimension정보를 줄수 있다는데, top에서 얼마만큼 떨어졌는지는 주어지지만, width는 모르지 않나? (edited) [12:44 AM]
Stack( children: [ Positioned( top: 30, bottom: 30, child: container(), )]) [12:45 AM] stack의 높이가 90이라고 하면 positioned에서 container의 height를 30으로 강제해 버린다. width도 동일한 방식으로 container의 width를 강제할 수 있다. [12:45 AM] If all three values on a particular axis are null, then the Stack.alignment property is used to position the child.
If all six values are null, the child is a non-positioned child. The Stack uses only the non-positioned children to size itself. [12:49 AM] 특정축에 있는 3개의 값이라? 그리고 6개의 값은 뭔지 모르겠다. [12:49 AM] top,left,bottom,right 4개 값 아닌가?
hoyoul — 11/16/2023 12:55 AM width와 height까지 합하면 6개의 값은 된다. [1:01 AM] 아! 특정축의 3개의 값이 null이란건, position 속성은 4개가 있다. 이중 3개가 null이라고 한다면, child의 position을 정할 수가 없다. 예를 들어서, top,bottom,left가 정해지지 않고, right: 10만 있거나, left:10만 있으면, 위치를 정할 수 었다. 이런 경우에 stack.alignment속성에서 child의 position을 정해준다는 얘기다. 6개가 null이란건 4개의 position정보와 width,height등, 어느하나라도 기술이 안되어 있다면, 그것은 non-positioned child라고 부른다. stack에선 non-positioned chilren들을 사용하는데, stack의 size로 사용한다? 마지막 size가 뭔지 잘 모르겠다.
hoyoul — 11/16/2023 1:05 AM size는 width와 height값인데, stack을 생성할때 width와 height를 줄 수 없다는 얘긴가? position 위젯이 아닌경우와 positioned widget이긴 하지만, 3개가 null값이라서 위치를 지정할 수 없는 positioned widget은 non-positioned child로 취급한다고 했고, 그 두개가 stack의 size를 결정한다. 뭐 이런거 같긴 한데… [1:06 AM] stack widget [continue…]
hoyoul — 11/16/2023 1:15 AM Each child of a Stack widget is either positioned or non-positioned. Positioned children are those wrapped in a Positioned widget that has at least one non-null property. The stack sizes itself to contain all the non-positioned children, which are positioned according to alignment (which defaults to the top-left corner in left-to-right environments and the top-right corner in right-to-left environments). The positioned children are then placed relative to the stack according to their top, right, bottom, and left properties. [1:16 AM] 여기서부터 다시한다. stack에는 자식들이 있다. 그 자식들은 2가지 종류가 있다. positioned와 non-positioned. 예를 들면, Stack( children: [ Positioned( child: Text(’test one’), ), Text(’test’), Row(), ]) (edited) [1:17 AM] 즉 Positioned child도 있고, Text,Row같은 non-positioned child가 있다.
11.16
하지만, 위처럼 Positioned widget으로 생성된 Text(’test one’)이 positioned child냐? 아니다. position에 해당하는 어떠한 정보도 제공하지 않는다. 따라서 non-positioned child다. [8:44 AM] stack은 크기를 기술하지 않는다. Stack widget의 크기는 non-positioned child에 의해서 정해진다. 예를 들어서, Stack( children: [ Container( width: 400, height: 300, ), Positioned( child: Text(’test’), width: 500, height: 500, )]) [8:45 AM] 두 개의 non-positioned child가 있다. 위치가 정해지지 않은 두개의 child중 큰 500x500의 Text가 Stack의 크기가 된다. [8:47 AM] 위치가 정해지지 않은 non-positioned child는 stack에 놓여질때, 어떻게 놓여지는가? Stack이 갖는 alignment란 속성이 있다. 이 속성은 child의 위치에 대한 기준을 준다. 만일 alignment 속성이 기술되어 있지 않다면, top-left corner가 default 기준이다.
hoyoul — 11/16/2023 8:49 AM Positioned 객체는 top,right,bottom,left라는 속성값을 설정하면, stack에서 position이 결정된다. [8:50 AM] The stack paints its children in order with the first child being at the bottom. If you want to change the order in which the children paint, you can rebuild the stack with the children in the new order. If you reorder the children in this way, consider giving the children non-null keys. These keys will cause the framework to move the underlying objects for the children to their new locations rather than recreate them at their new location. [8:53 AM] stack이란게 자식들을 겹쳐서 그릴수 있는데, 순서는 자식들의 위치한 순서에 따른다. 예를 들어서, Stack( children: [ Container( width: 300, height: 300, color: Colors.red, ), Container( width: 500, height:500, color: Colors.blue, )]) [8:53 AM] 제일 먼저 빨간색 container가 그려지고 그다음 blue container가 그려진다. [8:54 AM] 순서를 바꿀려면 children의 위치를 바꾸고 rebuild하면 된다고 한다. [8:55 AM] 여기서 key라는게 나오는데, 모든 widget은 key를 가지고 있다. 이 key는 widget의 identity를 나타낸다고 봐도 된다.
hoyoul — 11/16/2023 8:58 AM stateful에서도 dirty를 판정하고 build를 호출할때, widget의 key를 가지고 비교한다. widget의 key는 같은 종류를 나타낸다. 예를 들어서 위에서 container의 속성을 변경했다고 하자. build시에 element tree와 widget tree를 비교하는데, 속성만 변경되었고, widget의 종류는 같다. 즉 key가 같기 때문에 새롭게 widget을 만드는게 아니라, relocation하던지, 속성값만 다시 적용해주는 것이다. [8:58 AM] key를 변경했다면 recreate한다. [8:59 AM] For more details about the stack layout algorithm, see RenderStack.
If you want to lay a number of children out in a particular pattern, or if you want to make a custom layout manager, you probably want to use CustomMultiChildLayout instead. In particular, when using a Stack you can’t position children relative to their size or the stack’s own size. [8:59 AM] key와 관련한 stack layout은 RenderStack을 살펴보라고 했다. RenderStack은 Render Tree의 요소다.
hoyoul — 11/16/2023 9:11 AM stack에 많은 예를 들어, 100개의 children으로 어떤 pattern을 만든다거나, custom layout manager를 만들고 싶다면, CustomMultiChildLayout을 사용하면 된다고 한다. 즉 많은수의 children의 위치 처리를 쉽게 할수 있는거 같다. stack의 size를 기준으로 children의 위치를 정할수 없다는 말도 한다. 문단내의 문장들의 관계를 잘 모르겠다. 여튼 대충 해석하면 그렇다. [9:11 AM] 두개의 예제를 든다. [9:12 AM]

Figure 21: stack1
Stack( children: <Widget>[ Container( width: 100, height: 100, color: Colors.red, ), Container( width: 90, height: 90, color: Colors.green, ), Container( width: 80, height: 80, color: Colors.blue, ), ], ) [9:13 AM] stack의 순서대로 쌓아지고 보여진다. stack의 size는 100x100으로 정해진다. [9:13 AM] 두번째 예제를 보자.

Figure 22: stack2
SizedBox( width: 250, height: 250, child: Stack( children: <Widget>[ Container( width: 250, height: 250, color: Colors.white, ), Container( padding: const EdgeInsets.all(5.0), alignment: Alignment.bottomCenter, decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: <Color>[ Colors.black.withAlpha(0), Colors.black12, Colors.black45 ], ), ), child: const Text( ‘Foreground Text’, style: TextStyle(color: Colors.white, fontSize: 20.0), ), ), ], ), ) [9:15 AM] stack의 부모로 SizedBox가 있다. SizedBox는 250x250의 크기를 갖는다. 따라서 Stack은 최대 250x250의 크기를 가질 수 있다.
hoyoul — 11/16/2023 9:18 AM 제일 처음 하얀색 container가 250x250의 크기로 덮여지고, 그다음 container가 덮여지는데, 검은색 color gradient를 나타낸 container다. 그 다음 Text를 덮는다. [9:20 AM] Stack과 Positioned Widget을 공부했으니, 이제 CardsInHand라는 component를 만들수 있어야 한다.

Figure 23: stack3
CardsInHand component
hoyoul — 11/16/2023 9:36 AM 손에 쥐고 있는 카드는 처음엔 없다. 첫번째 turn때 한장씩 받는다. 그 다음 turn때 또 한장을 받는다. 이 위젯은 보여줄 카드 수를 인자로 받게 해서, 예를 들어서 0을 입력하면 아무것도 안보여주고, 1을 전달하면 한장만 보여주고, 2를 전달하면 두개의 카드를 겹쳐서 보여주게 하면 된다. [9:36 AM] 인자에 해당하는 변수를 정해야 한다. numberOfShowCards라는 현재 turn에 보여줄 card수를 멤버변수로 잡자. [9:39 AM] 또 하나 고려해야 할께 있다. other player의 카드는 크기가 작고, 겹쳐져 있다. 반면에 player의 card는 크기가 크고, 떨어져 있다. 이것도 인자로 받아야 한다.
hoyoul — 11/16/2023 9:45 AM 인자에 따라서 if문을 사용해서 배치를 다르게 하면 될듯하다. [9:48 AM] if (playerType == ‘other’){ for (i = 0; i< numberOfShowCards; i++){ //작은 카드를 stack으로 처리.. } } else{ for (i = 0;i< numberOfShowCards; i++){ //큰 card를 떨어뜨려서 표시 } } [9:48 AM] 어떻게 처리할지는 좀 고민해야 할듯하다. [9:50 AM] 10분만 쉬자.
hoyoul — 11/16/2023 9:53 AM 그리고 player type은 상태 data다. player type에 따라서 ui가 달라지기 때문에, 이것은 나중에 다 model에 넣어야 한다.
hoyoul — 11/16/2023 10:06 AM 좀더 머리속을 정리해야겠다. CardsInHand component는 2개의 인자가 필요하다. 보여줄 카드의 수, 그리고 player의 type…그리고 stack을 return하면 된다. [10:11 AM] 큰카드던 작은 카드던 모두 stack으로 처리한다. stack의 child는 positioned객체이거나 아닐수 있는데, 둘다 positioned객체로 해야한다. positioned객체에서 position속성을 명시해서 겹쳐진거, 떨어진것을 표시해야 한다.
hoyoul — 11/16/2023 10:31 AM stack 사용시 에러가 난다. ══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════ The following assertion was thrown during performLayout(): ‘package:flutter/src/rendering/stack.dart’: Failed assertion: line 598 pos 12: ‘size.isFinite’: is not true.
Either the assertion indicates an error in the framework itself, or we should provide substantially more information in this error message to help you determine and fix the underlying cause.
The relevant error-causing widget was: Stack Stack:</Users/hoyoul/Development/Projects/kholdem-front/lib/components/cards_in_hand_widget.dart:51:12>
When the exception was thrown, this was the stack: #2 RenderStack._computeSize (package:flutter/src/rendering/stack.dart:598:12) (edited)
hoyoul — 11/16/2023 11:03 AM 간단한 테스트도 에러가 난다. [11:03 AM] Widget build(BuildContext context) { return Stack( children: [ Positioned( left:0, child: const CardWidget( cardNumber: 0, isPlayerCard: false, ), ), Positioned( left: 30, child: const CardWidget( cardNumber: 0, isPlayerCard: false) ), ], ); } [11:03 AM] CardWidget을 이용하는 간단한것도 에러가 난다.
hoyoul — 11/16/2023 11:20 AM 문제는 해결했는데, 정확한 이유를 모르겠다. (edited) [11:20 AM]

Figure 24: stack4
겹쳐지게 했는데, 이건 test로 staic하게 했고, 동적으로 구현한건 에러가 있다. 그리고 바뀐것은 stack을 바로 return하게 하지 않았다. sizedbox로 둘러싼 후 return했다. sizedbox를 둘러싼건 stack의 size를 정하기 위함이다. 이전에 바로 stack을 return했을때 에러가 난 이유는…내 생각은 다음과 같다. stack은 size가 정해지지 않는다. non-positioned객체가 있는경우, non-positioned child의 size에 의해 결정된다. 그런데 내 stack의 children은 카드위젯이고, 이 카드 위젯들은 size가 있다. 그리고 children을 positioned객체로 만들어서, left속성을 줬다. 즉 모두 Positioned객체다. 이런 경우는 stack의 size를 명확하게 기술해야 하는 것 같다. nonPositioned객체가 하나라도 있다면 stack의 size가 정해지지만, Positioned객체만 있는경우는 size를 명시해야 한다. 그래서 나는 Stack위에 SizedBox를 부모로 해서 size를 명시적으로 기술했다. 이건 그냥 추론일 뿐이다. 내 뇌피셜이다. 아래는 내 테스트 코드다. 물론 이건 에러가 너무 나서, 아니 이정도는 동작해줘야 하는거 아니야라는 마지노선으로 테스트한거라서 원래 의도와는 다르다. (edited)
hoyoul — 11/16/2023 11:30 AM import ‘package:flutter/material.dart’; import ‘/components/card_widget.dart’;
class CardsInHandWidget extends StatelessWidget { final int numberOfShowCards; final String playerType; * component의 모든 데이터는 외부로부터 생성자로 제공하는 식으로 설계되어야 한다. component안에서 상태 데이터를 가공 처리하면 안된다. 따라서 required로 처리해야 한다. *
const CardsInHandWidget({ Key? key, required this.numberOfShowCards, required this.playerType, }) : super(key: key);
@override Widget build(BuildContext context) { return SizedBox( width: 80, height: 100, child: Stack( children: <Widget>[ Positioned( top: 0, left:0, child: const CardWidget( cardNumber: 3, isPlayerCard: true, ), ), Positioned( top: 0, left: 20, child: const CardWidget( cardNumber: 5, isPlayerCard: true), ), ], ), ); } } [11:32 AM] compoent를 만들어서 쓰기때문에 코드는 심플하다. [11:36 AM] 내생각이 맞는거 같다. [11:36 AM] stack 사용시 주의사항 [! important ] (edited)
hoyoul — 11/16/2023 11:37 AM stack에는 width,height 속성이 없다. stack도 container처럼 액체괴물이다. 즉 기본적으로, content의 크기에 맞춰 자신의 size가 결정된다. 하지만 container와 다른게 있다. (edited) [11:38 AM] stack이 container와 다른건, 자식들이 두 종류가 있다는 것이다. Positioned 자식은 자신의 위치가 명확히 결정된 자식이고 non-Positioned 자식은 size나 위치가 명확하지 않은 자식이다. non-positioned자식만 있다면 container와 비슷하다. (edited) [11:41 AM] stack의 자식들이 non-Positioned만 있다면, 그냥 스택에 겹쳐서 쌓여져서 제일 마지막 자식만 보여진다. 그리고 stack의 size도 가장 큰 자식을 따른다. 이건 문제가 없다. 그냥 사용하면 된다. 그런데 이런 의도로 stack을 사용하는 경우는 포토샵의 layer처럼 각각의 자식들의 alpha효과를 이용한 작업일때만 해당한다. (edited)
hoyoul — 11/16/2023 11:46 AM stack을 사용해서 자식들이 겹쳐지는 효과를 사용한다면, 예를들어,지금 우리 게임에서 겹쳐진 카드형태처럼 표현하려면, 자식들이 position을 명확하게 가져야 한다. 그래서 Positioned객체로 만들어서 사용한다. 여기서 문제가 발생한다. stack의 자식들은 명확한 위치와 size를 가지고 있지만, stack은 그렇지 않다는 것이다. stack은 size속성이 없기 때문이다. 그래서 부모로 SizedBox같은걸 이용해서 size를 처리해줘야 한다. 명시적으로 기술해줘야 한다. [11:47 AM] 그럼 설계가 달라진다.

Figure 25: stack6
import ‘package:flutter/material.dart’; import ‘/components/card_widget.dart’; import ‘dart:math’;
class CardsInHandWidget extends StatelessWidget { final int numberOfShowCards; final bool isPlayer; final Random? random; * component의 모든 데이터는 외부로부터 생성자로 제공하는 식으로 설계되어야 한다. component안에서 상태 데이터를 가공 처리하면 안된다. 따라서 required로 처리해야 한다. playercard: 60x90 otherplayercard: 48x70 player Area: 128x90 (max:2장 받았을때, 카드사이의 공간:8) other player area: 68x70 (max: 2장받았을때 : 두번째카드 시작 위치:20) *
const CardsInHandWidget({ Key? key, required this.numberOfShowCards, required this.isPlayer, this.random,
}) : super(key: key);
@override Widget build(BuildContext context) {
List<Widget> cards = []; final randomInstance = random ?? Random();
/ Other player card는 card(48) - (28: 겹쳐진 부분)으로 표시 if (!isPlayer) { for (int i = 0; i < numberOfShowCards; i++) { cards.add( Positioned( left: i * 20.0, child: CardWidget( cardNumber: randomInstance.nextInt(12) + 1, isPlayerCard: false, ), ), ); } } / player card는 60(card width) + 8(떨어진 간격)으로 배치 else { for (int i = 0; i < numberOfShowCards; i++) { cards.add( Positioned( left: i * 68, child: CardWidget( cardNumber: randomInstance.nextInt(12) + 1, isPlayerCard: true, ), ), ); } }
return SizedBox(
width: isPlayer ? 128.0 : 68.0,
height: isPlayer ? 90 : 70,
child: Stack(
children: cards,
),
);
} } [2:25 PM] 이 component는 입력으로는 isPlayer와 numberOfShowCards가 쓰인다. [2:27 PM] Player와 otherPlayer가 있는데, 둘의 차이는 카드크기와 layout이 달라진다. 그리고 몇장의 카드를 보여줄지를 입력 받는다. 입력받으면 random으로 이미지를 선택해서 현재는 보여주게 했다. 뒷면만 보여주게 바꾸긴 해야 한다. [2:27 PM] 사용법은 다음과 같다. [2:27 PM] CardsInHandWidget( numberOfShowCards: 2, isPlayer: true, ), CardsInHandWidget( numberOfShowCards: 2, isPlayer: false, ), [2:29 PM] isPlayer가 true면, Player card쌍이 보이고, false면 otherplayers의 card쌍이 보인다. other player도 1,2,3,4가 있지만, 여기서는 따로 처리하지 않는다. [2:30 PM] 뒷면을 출력하게 하면 slot이 없으니까, 작은 카드의 경우는 겹쳐진다. 색을 다르게 하던가 slot을 만들던지 해야할듯하다.

Figure 26: stack5
나는 other player card나 player card나 동일한 뒷면이라고 생각해서 player카드 뒷면을 이미지로 추출해서 사용했는데, 아니였다. 다른 색상이다. 이것도 고쳐야 하고, slot도 표시해보자.

Figure 27: stack7
figma는 카드에 shadow가 되어 있다. 이정도만 하고 넘어가자. 나중에 model때문에 다시 변경해야 하기 때문이다. 지금 2개정도 component를 더 만들어야 한다. action component와 bet component다. [3:14 PM] playerBetComponent

Figure 28: stack8
3개의 입력이 필요하다. [3:27 PM] 순서를 나타내는 text, user명, betting금액 [3:27 PM] 출력은 container다.
hoyoul — 11/16/2023 3:41 PM currentTurn, playerName, betAmount로 네이밍하자. [3:41 PM] bettingAmount가 더 나은거 같기도 하다.
hoyoul — 11/16/2023 4:57 PM component 만들시 주의사항 [4:59 PM] compoent에서 생성자로 입력받을 때 primitve type을 되도록 피하자. 예를 들면, string으로 입력받기 보다, Text형태로 입력받는게 더 유연한 처리가 가능하다.

Figure 29: stack9
component를 만들긴 했는데, 1st를 null safety처리했다. 왜냐면 null일수도 있기 때문에, 그런데 null safety를 하면 playerName이 그 공간을 expand처리해야한다. 그런데 이런것들이 다 business logic인데, 왜 view에서 이렇게 공을 들이느냐? 이렇게 짜두면 model로 이동할때 편할꺼 같아서 하는것이다. 왜냐? 동작이 검증되었기 때문이다. [8:27 PM] 축구한다니까…축구보고 이어서 하자.

Figure 30: stack10
위 코드에서 눈여겨볼건 spread연산자를 사용해 봤다라는 건데, 어차피 모델로 옮길꺼라서 의미는 없다. 그래도 정리할 필요는 있다. (edited) November 17, 2023
hoyoul — 11/17/2023 8:48 AM Spread Operator (edited)
hoyoul — 11/17/2023 9:10 AM https://www.geeksforgeeks.org/dart-spread-operator/ https://aostols.tistory.com/21
두개의 site를 참고 했다. 첫번째는 spread의 문법적 내용이고, 두번째는 활용법이다. [9:12 AM] Row( children: [ if (currentTurn != null) …[ currentTurn!, const SizedBox(width: 10), ], Expanded(child: playerName), ], ), [9:14 AM] 내 코드는 다음과 같다. currentTurn은 ‘1st’와 같이 현재 turn(순서)를 표시해야 하는경우를 말한다. 다른 user는 순서를 표시하지 않기때문에 그런경우는 null값이 되는데, 이때 Text는 확장해야 한다. 그래서 위와 같이 처리를 했다. [9:16 AM] 1st를 사용해서 순서를 나타내는 turn flag를 뺄수도 있다고 했다. 그러면 배경색이나 어떤 다른 방식을 사용하거나 아예 안 사용할 수 있다고 했는데, 그건 그때 고치면 된다.
hoyoul — 11/17/2023 9:25 AM Action Widget Component

Figure 31: stack11
구조는 다음과 같다. 현재 자신의 종잣돈?에서 걸수 있는 betting 금액을 보여주고, 액션들이 나와 있다. 입력은 betting금액을 나타내는 Text widget, action을 나타내는 container로 보면된다. [9:53 AM] naming을 하자. [9:55 AM] bettingAmount, actionAmount, actionForBetAmount로 길게 해도 되고…. [9:57 AM] actionItem, actionKind, actionText로 해도 되고..그렇다.
hoyoul — 11/17/2023 9:58 AM actionAmountText, actionText라는 두개의 입력을 받게 하자. [9:58 AM] postfix를 붙여서 명확하게 하자. [10:01 AM] component 만들때 주의 사항- stack, container (!important) [10:02 AM] container와 stack과 같은 layout을 처리하는 widget을 다룰때, 유의할 께 있다. [10:04 AM] container와 stack, row, column등도 마찬가지긴 한데, 여러개의 child를 가질수 있는 container계열의 widget들의 size는 자식들에 의해 결정되는 경우가 많다. 즉 동적으로 결정될 수 있다. 그래서 runtime error가 종종 발생한다. (edited)
hoyoul — 11/17/2023 10:06 AM 그래서 container계열의 widget을 만들어서 return하는 경우, sizedbox로 둘러싸서, size를 명확히 해서 return하거나, container나 몇몇 widget들은 size를 명시할 수 있기 때문에 외부로 부터 width, height를 받아서 명시해서 사용해야 한다.

Figure 32: stack12
마지막으로 playerSeat를 만들면, page를 만들수 있을꺼 같다. [11:16 AM] PlayerSeat component

Figure 33: stack13
PlayerSeat는 2종류의 type이 있다. Player와 OtherPlayer로 나눌수 있다. Player는 User로 보면되고, 카드 크기도 크고, seat도 크다. OtherPlayer는 카드도 작고 playerBet component가 포함되어 있다. [11:37 AM] 또한 OtherPlayer의 seat를 보면 turn을 나타내는 색이 배경색으로 되어 있다.
hoyoul — 11/17/2023 1:35 PM 입력으로는 isPlayer라는 bool값과, isTurn이란 bool값만 있으면 될듯하다. [1:36 PM] 출력은 container를 return하면 될듯하다.

Figure 34: stack14
만들어 봤는데, 조정이 필요하다. 우선 판의 크기를 조금크게하고 두개의 widget을 붙이고, 겹쳐진 화투패도 조정해야 한다.

Figure 35: stack16
GamePlayScreen 만들기. [3:42 PM] 이제 만든 component를 조합해서 page를 만들면 된다.
hoyoul — 11/17/2023 4:22 PM 참고: row나 column은 배경색이 없다. 배경색은 container를 사용해야 한다.

Figure 36: stack17
좀 이상한게 몇개 있다.
hoyoul — 11/17/2023 8:07 PM 모든 widget의 입력은 가능한 primitive아닌 widget형태로 받는다. 예를 들어서 String을 안사용하고 Text를 사용하는데, Text widget은 속성처리도 꽤많다. 외부에서 생성시 이런 속성을 다 처리하니, 코드가 지저분해졌다. 생각해보니, Text가 가장 중복이 많은 widget임에도 불구하고 Text widget을 component로 안만들었다. 만들어야 할듯. [8:07 PM] 상단의 pot, call의 배경색을 빼먹었다. [8:10 PM] 카드 겹침 효과를 주기위해서 의도적으로 두번째 카드를 내려서 보여주는데, 같은 위치로 한다. 같은 위치로 하면 색이 비슷해서 이어지는 모양이 되는데, figma에선 카드가 겹쳐지는 부분에 shadow효과를 주었다. 찾아보고 적용하자. [8:10 PM] 약간의 location 위치가 어긋난게 있다. [8:11 PM] 요것만 하고 git에 올리자. 어차피 디자인은 계속 변경될거니까… [8:12 PM] 내일 오전에 해야겠다. 8:30분까지는 못할듯하다. [8:13 PM] 오늘은 money label 소스 변경하고, 백그라운드 처리, location, Text위젯을 만들고 연관된 부분 소스변경만 하자.
hoyoul — 11/17/2023 9:10 PM dart 생성자에 대해서 (edited) [9:11 PM] MyWidget이란 class가 있을 때, 생성자는 MyWidget(){} 이렇게 보통 많이 사용한다. 하지만 dart는 좀 다르다. (edited) [9:12 PM] MyWidget({ required this.temp }); =>이런식으로 많이 사용한다. body가 없다.
hoyoul — 11/17/2023 9:20 PM MyWidget(int name); [9:20 PM] 위와 같은 생성자는 이 객체를 생성할때 name이란 argument가 필수다. 그리고 초기화도 안해준다. [9:21 PM] MyWidget({this.name}); [9:21 PM] 이와 같은 생성자는 name이란 argument가 option이다. 그리고 값이 있다면 초기화 해준다. [9:21 PM] MyWidget({required this.name}); [9:22 PM] 이건 argument가 필수다. 그리고 초기화도 해준다. [9:22 PM] initializer list [9:23 PM] dart의 생성자는 body가 없다. 그러면 아까 봤던 첫번째 경우에는 초기화를 못하는 걸까? [9:24 PM] final int name; MyWidget(int name)
name = name;
[9:24 PM] 이렇게 초기화 시켜준다. November 18, 2023
hoyoul — 11/18/2023 9:40 AM figma design 적용시 생각해야 할것 [9:40 AM] 디자인을 받았을때, 중복이 되는것을 어떻게 처리할 것인가? [9:41 AM] 반복이 되는것을 themedata로 처리할 수 있겠고, 객체로 처리할수도 있다. [9:46 AM] 나는 객체로 처리하는 방법, 즉 component라는 widget을 만들어 재사용했다.
hoyoul — 11/18/2023 9:47 AM component란 widget들을 재사용하는 경우, 가장 작은 atom단위부터 만들고, 조합해서 더 큰 component를 만드는 방식을 사용했다. [9:49 AM] 가장 작은 Text를 처리하지 않아서 ,다 만든다음에 Text를 생성하다 보니, 코드수정도 많고 꼬이기도 했다. 작업량이 의외로 많다. [9:51 AM] 그런데 theme을 사용하는게 적절해보이고 가독성도 높은거같다. [9:52 AM] 물론 지금 바꾸진 않겠는데, [9:52 AM] https://www.figma.com/community/plugin/1208619373237156795/very-good-flutter-styles
figma에 이런 plugin이 있다. 즉 designer가 만든 design에서 사용되는 sytle요소를 뽑아낸다. 이것을 themedata로 만들어쓸때 유용할꺼 같다. 저건 전체적인 theme에 사용되는 style을 뽑아주고, theme data는 내가 만들긴 하지만, 작업속도가 더 빨라질듯 하다. [9:54 AM] 그런데, 테스트 해보니…
사용법을 따라했는데 안된다. (edited)
hoyoul — 11/18/2023 9:54 AM design이나 dev모드에서 실행하라. [9:55 AM] color, text styles버튼을 누르면 clip보드에 복사가 될거다. [9:55 AM] 에디터에서 붙여 써라. [9:56 AM] 그런데 동영상에서 보이듯이 plugin을 dev모드에서 실행해도. 복사할 수 있는 스타일이 0개다.
11.18
text만 위젯으로 만들고 적용하는것만 하고, ui는 todo만 남긴채 commit을 올려야겠다. 그리고 점심 이후에는 model만들고 적용하는거 해야할듯하다. (edited)
hoyoul — 11/18/2023 11:15 AM Text 의 종류 TitleText font: inter, color: 0xffffffff, size: 27, weight: black(FontWeight.w900),
Logo button Text (google, apple, join) font: inter color: 0xff272731(구글,join), 0xffffffff (애플) size:16, weight: semibold(FontWeight.w600)
Money Text (action money, other-player money, pot-call money, 3 other의 3) font: inter color: (0xff3d3d44, action-money) , (0xff858588-otherplayer, pot-call money), size:12 weight: semi-bolo(font weight.w600)
Flag Text( 1st, win, user, pot, call) font: inter, color: 0xff43cfA5(1st,win), 0xffffffff(user, pot, call) size:10 Weight: bold=> fontweight700 action Text(Die,Double…) font: Inter, color:0x3d3d44 size:16 weight: bold,fontweight.700
WaitingNumberText font: inter, color: 0xff4723B4, size:12 weight:semibold=> w600
LeftSeatNumberText font:inter color: 0xffd8d8da size: 14 weight: regular=> w400
General text “3 other player waitings” => 3은 waiting number Text “2 Seat left” => left seat number Text font: inter, color: 0xff9e9ea2=> other player waiting, 0xffd8d8da => seat left, size: 12=>other, 14=>seat left, weight: medium=>other w500, regular=>seat left w400 (edited)
hoyoul — 11/18/2023 11:37 AM import ‘package:flutter/material.dart’;
class TitleTextWidget extends StatelessWidget { final String text;
const TitleTextWidget({required this.text});
@override Widget build(BuildContext context) { return Text( text, style: TextStyle( color: Colors.white, fontSize: 27, fontWeight: FontWeight.w900, ), ); } } (edited) [11:37 AM] title같은 경우는 간단하게 이렇게 구성하면 되는데, 나머지는 type에 따라 다른 속성이 적용되게 바꿔야 한다. (edited)
hoyoul — 11/18/2023 11:48 AM logobuttonText import ‘package:flutter/material.dart’;
class ButtonTextWidget extends StatelessWidget { final Color color; final String text;
const ButtonTextWidget({ required this.color, required this.text, Key? key, }) : super(key: key);
@override Widget build(BuildContext context) { return Text( text, style: TextStyle( color: color, fontSize: 16, fontWeight: FontWeight.w600, ), ); } } (edited) [11:49 AM] color를 외부로 부터 입력하는게 맞는지는 모르겠어서 flutter 코드중에 color를 밖에서 입력하는경우는 많음. 문제 없음. 그리고 이전예는 그냥 생각난대로 짜서 key에서 에러가 났음. 이건 에러가 없음. [11:50 AM] ButtonTextWidget으로 이름 바꿈. [11:50 AM] logo만 있는것이 아니기 때문에…
hoyoul — 11/18/2023 11:59 AM color를 themedata로 빼내는 것은 어떨까? [12:00 PM] hexa값을 직접사용하는건, 아닌거 같다. 그렇다고 static final string으로 java처럼 작성하는건 아닌거 같다. [12:01 PM] theme을 이용하는게 맞긴한데…즉 app에서 사용하는 모든 color, font, primitive한 ui 요소들은 themedata로 빼내고, 중복되는것은 widget단위로하는게 더 유연성이 좋아보이긴 하다.
hoyoul — 11/18/2023 12:09 PM 우선 밥먹자. [12:11 PM] 여튼 지금 고민은 themedata의 활용과 component widget을 어떻게 조화롭게 사용하느냐? themedata만 사용하느냐, component만 사용하느냐도 고민했지만, themedata라는게 app에 전체적으로 적용되는 ui란 의미라서, font나 color같은 전체적으로 적용되는 어떤 ui지, 이것이 font widget이나 color widget을 custom해서 사용할 필요는 없다는 것이다.
hoyoul — 11/18/2023 12:26 PM 내가 하고싶은건, 한마디로 코드의 가독성을 높이고 싶다. 딱 보면 딱딱딱 해석이 되야 한다. 그래야 나도 코드 수정이 편하고 남이 봐도 이해가 된다. 복잡하면 안된다. 그래서 page소스를 봤을때, 딱딱딱 매핑이 되야, 아 어디가 문제 있구나, 어디를 고치면 되겠다. 이게 나온다. [12:28 PM] 그러면 사용하는 어떤 위젯을 보더라도, 내부구조가 보여야 한다. 예를 들어…아 이건 logobutton이구나. 내부를 보니 logobuttonText가 있고, logo icon도 있고…이렇게 해야 하는데… [12:32 PM] custom widget의 입력으로 Text를 사용하는데, text에는 단지 string만 있는게 아니라, style: TextStyle속성을 기술하는데, 이게 같은 코드가 속성값만 다르게 해서 page를 구성하니,, 지저분하고 중복이기 때문에 Text를 component화 하는건 맞다. 여기서 theme을 사용할수 없는게, theme은 UI에서 Text가 거의 천편일률적으로 사용되는 경우면 theme을 사용하는게 맞다. 다이얼로그박스가 독특하게 디자인됐는데, 이게 전반적으로 쓰인다면 theme에서 사용해야 한다. 마치 전체 app에서 font나 background컬러가 사용되는것처럼. 이런 경우가 아니라면 그냥 widget을 만드는게 맞다고 본다.
hoyoul — 11/18/2023 12:34 PM 그리고, 내가 component의 postfix로 widget을 붙였는데, component를 붙이는게 더 맞다. 그리고 page에는 screen을 붙였는데, page를 붙이는게 더 낫다. 이게 더 가독성이 높고 folder이름도 pages, components다. 이건 수정하는게 맞다. [12:36 PM] 오늘 이런걸 수정하더라도 하고서 commit 올리는게 맞다. 이렇게 놔두고 다른걸 하면 햇갈리고 다시 소스를 생각해야 한다. 어느정도 내가 원하는걸 만들고 나서 다른 작업이 들어가는게 맞다. 이것은 어차피 해야하는 일이다. 그래서 건너뛸 필요는 없다. 언젠간 해야하는거다.
hoyoul — 11/18/2023 1:45 PM
- Text widget처리
hoyoul — 11/18/2023 2:45 PM themeData로 ui의 일부를 처리하고, widget으로 themedata에서 가져와서 사용하는 식을 생각했는데…그냥 factory 패턴으로 TextWidget을 return하는 class를 만드는게 더 속편할수도 있을꺼 같다. [2:47 PM] factory패턴을 사용해서 그냥 종류하고 원하는 text만 입력하면 해당하는 속성을 채워주는 Text widget을 return하는 것이다. enum을 사용해서 처리하는 text의 종류를 나열하면, 가독성도 좋고, 즉 공장에서 return하는 Text의 종류도 알기 쉽고… (edited)
hoyoul — 11/18/2023 5:10 PM import ‘package:flutter/material.dart’; enum TextType { titleText, / ex) “Log in or Sign up”, “2 Card Play” googleButtonText, / ex) “google” “join” appleButtonText, / ex) “apple” actionNumberText, / ex) number on action text like “30 Die” potcallNumberText, / ex) number on pot or call on the top “pot 3000” playerFlagText, / ex) 1st, win potcallText, / ex) pot, call actionText, / ex) Die,Call waitingNumberText, / ex) 3 in “3 other player waiting” leftSeatNumberText, / ex) 3 in “3 seat left” waitingText, / ex) except 3 in “3 other plyaer waiting” leftSeatText, / ex) except 3 in “3 seat left” }
class TextFactory { static Text createText(String text, TextType type) { TextStyle textStyle;
switch (type) {
/ titleText case TextType.titleText: textStyle = const TextStyle( color: Colors.white, fontSize: 27, fontWeight: FontWeight.bold, ); break; / googleButtonText: case TextType.googleButtonText: textStyle = const TextStyle( color: Color(0xff272731), fontSize: 16, fontWeight: FontWeight.w600, ); break;
// 계속 이렇게…
// leftSeatText case TextType.leftSeatText: textStyle = const TextStyle( color: Color(0xffd8d8da), fontSize: 14, fontWeight: FontWeight.w400, ); break;
default: textStyle = const TextStyle( color: Colors.black, fontSize: 14, fontWeight: FontWeight.normal, ); }
return Text(
text,
style: textStyle,
);
} } [5:12 PM] app에 사용하는 font들이 weight와 color를 고려하면 많이 사용된다. 여기엔 일부지만, 여튼 다 factory에 집어넣다. 내가 원하는건, text를 사용할때, 원하는 string, 종류만 입력하면 text가 나오면 된다. [5:12 PM] 이건 복잡해도 된다. 하지만, 이걸 통해서 나오는 코드는 간결해야한다. [5:13 PM] 근데 머리아픈게 이렇게 팩토리로 짜면 getX를 사용할때가…문제가 될수 있다. (edited)
hoyoul — 11/18/2023 5:21 PM 예를 들어서 pot을 나타내는 변수를 만들건데, 이것은 사용자의 판돈이라고 하자. 초기화 과정에서 이 변수는 0이다. 그런데 rails에 요청에서 사용자가 가진 판돈을 가져와서 이변수에 변경을 해주면, view에서 자동으로 반영된다. 물론 내가 obx로 widget만들어 주었다면 말이다. 근데, obx에서 factory를 사용해서 반영하게하면 더 간단해 진다.. (edited) [5:22 PM] 오히려 getX를 쓰기 더 좋아보이긴 한데…해봐야 알꺼 같다. [5:22 PM] 쓰고보니 잘한거 같기도 하고…
11.19
Figma를 받았을때 무엇을 생각해야 하는가? [9:11 AM] 나는 동일하게 구현하는데만 방점을 찍었다. 왜냐면 많은 경험이 없으니까…그래서 구글링과 open chat에서 사람들의 의견을 구할수 밖에 없었다. 결과는 figma에서 필요한 색상, size 같은 정보를 얻어서 사용하면 된다는 것이였다. 그래서 그렇게 했다. 그런데 좀 다루다 보니, 이런 정보만을 얻는건 아닌거 같다. 왜냐면 구현하는데 있어서 고려해야할께 생기기 때문이다. [9:12 AM] 지금 나에게 누군가가 figma를 받고 어떤 생각을 해야 하나요?라고 묻는다면 나는 무슨말을 해야할까?
hoyoul — 11/19/2023 9:13 AM 첫번째로 figma에서 사용되는 모든 color, font, 같은 style정보를 끄집어 내서 나열해야 한다. (edited) [9:16 AM] 왜냐면, designer는 ui를 만들때, 일관성을 중요하게 여긴다. 그래서 반복된 UI를 사용하게 된다. 그래서 나열된 style은 이 app, web에서 consistency를 유지하는 pattern이라고 보면 된다. 이것을 쉽게 나열하는 툴도 있다. [9:17 AM] 예를 들면, https://www.figma.com/community/plugin/1208619373237156795/very-good-flutter-styles
이것을 사용할려면, designer가 style을 정의하고 사용하는 경우만 될꺼 같다. 예를 들면, 협업을 할때, designer도 자주 반복되는 것을 style로 정의하고 사용하는 거 같다. 그래서 그 style을 뽑아내는 건데, 내가 받은 figma에는 그런 style정보가 없어서 사용하지 못한다.
ps: 이 과정이 중요한 이유는 여기서 뽑아낸 정보가 아래에서 theme과 custom widget으로 다사용되기 때문이다. 위 tool이 된다면, 쉽게 뽑아내지만, 안그러면 나처럼 수동으로 하나하나 뽑아내야 한다. (edited)
hoyoul — 11/19/2023 9:20 AM 두번째, 디자인에서 반복되는 요소를 찾는다. about theme 동일한 형태나, 동일한 style을 갖는 요소들은 code에서도 반복적으로 사용된다. (edited) [9:22 AM] code에서는 반복을 매우 싫어한다. 반복을 하지 않게 하기 위해서 Theme과 class를 제공한다. [9:26 AM] 반복을 피하는 두개의 구조중에 어떤걸 사용해야 하는가? 상황에 따라 다른가? 왜 두개인가? 여러 질문들이 있을 수 있다.
hoyoul — 11/19/2023 9:29 AM 내가 내린 결론은 방법론이긴 한데, Theme이란건 획일적 요소에 사용된다. 예를 들면 app의 모든 page의 배경색은 blue다. 모든 font는 Iter font다. 모든 container는 둥근형태다. 등등 app에서만 사용되는 획일적인 요소를 Theme에 넣는다. Theme에는 각 요소에 style을 입힐수 있는, ScaffoldThemeData같은 것들을 제공한다. [9:31 AM] 그런데 figma design을 보면 모든게 획일적이지 않다. 2-3개의 background color를 갖는 page도 사용될 수 있고, 다른 모양의 2-3개의 dialog도 사용될 수도 있고 font가 2개 3개 사용될 수도 있다. 그러면 이경우 Theme을 사용하지 않는것인가? 아니다. Theme은 default로 생각해야 한다. [9:32 AM] 1)에서 디자이너가 사용한 style중 가장 많이 사용되는 font, style같은것을 Theme에 정의해서 default로 사용하는 것이다. [9:33 AM] 즉 Theme에서 모든 요소들을 정의해서 사용하는게 아니다. 가장 많이 사용되는 것들을 theme에 정의해서 중복을 막는 것이다. 참고로, flutter의 Theme에서는 widget에 관해서도 정의가 가능하다. 일종의 재료에 해당하는 font, color만 얘기하는게 아니다. widget도 theme에서 처리가 가능하다. (edited)
hoyoul — 11/19/2023 9:37 AM default를 정했으면, default에 해당되지 않는 중복요소들이 많다. 예를 들어서 내 경우는 round button이 한 3종류 4종류가 반복 사용된다. 이런 경우 가장 많이 사용되는 container의 style을 default로 theme에 정의한다. [9:38 AM] 그리고 나머지는 class를 만들어서 사용한다. 이 class는 flutter와 같은 UI관련 sdk에선, widget이라고 보면된다. 즉 custom widget을 만들어서 재사용하는 것이다. [9:40 AM] 내가 고민했던게 어떨때, theme을 사용하고, 어떨때 custom widget을 사용하느냐?였다. [9:40 AM] 그것은 theme은 default라고 생각하면서 해결되었다. 참고로 나는 theme에 2개밖에 설정을 하지 않았다. font와 scaffold의 background color다. default로 사용될 여러개의 theme을 만들 수도 있지만, 난 최소한으로 했다. 그 이유는 theme의 사용법에 있다. theme을 정의하고 사용할때는 Theme.of(context)와 같은 형태를 사용한다. 이게 낫설었고, 가독성이 좋지 않아 보였다. 그리고 하위 widget에서 매번 Theme.of를 사용하는것 또한 중복이다. 그래서 최소한으로 했다. (edited) [9:41 AM] about custom widget (component)
hoyoul — 11/19/2023 9:51 AM 이제 남은 반복요소를 custom widget으로 만들면 된다. 그래서 나는 중복되는 widget을 component란 이름으로 만들어서 사용했다. 그런데 가만히 보면, 중복되는 요소들이 다른 중복되는 widget을 사용해서 만들어지는 것이다. 예를 들어, 우리게임에는 player seat란는 게 있다. player가 손에쥔 카드와 유저정보를 보여주는데, 이게 player수만큼 만들어지고, 각 player seat마다 카드쌍, 유저정보가 들어간다. 거의 비슷한 형태의 player seat가 4개 5개가 만들어지는데… [9:54 AM] 이게 중복된 요소들로 이루어졌다. 따라서 내 결론은 가장 작은 custom widget을 만들어서 사용하자. 라는 것이였다. 그래서 가장 작다고 생각하는 요소를 만들고 이를 조합해서 큰 위젯을 만들었다. 이렇게 하니 코드가 깔끔해졌다. 하지만, 문제가 있었다. 모든 custom widget에 들어가는 인자로 되도록 widget을 받는다. 왜냐면 primitive data를 받아서 custom widget안에서 또다른 widget을 만들면 dependency가 생겨서 상태데이터를 처리할때 문제가 생긴다. 그래서 아예 독립적인 unit인 widget을 외부에서 만들어서 제공해야 한다고 한다. 예를 들어서, string을 받아서 custom widget에서 Text를 만들어서 사용될 수 있는데, 그러면 Text란 위젯은 custom 내부에서 만들어서 사용하는데, 나중에 getX나 다른 상태데이터를 다룰때 문제가 되기 때문이다. 그래서 외부에서 Text를 만들어서 custom widget에 인자로 넘겨주는 방법을 사용하는데, 이게 문제가 되었다. 왜냐? page에선 이런 custom widget에게 Text를 넘겨주는데, Text는 너무나 많은 다양한 종류에 중복이 2-3개씩 된다. 그러면 하나하나 다 생성해서 custom widget에 넘겨준다. 이게 코드를 너무 길게 만들었다. 그리고 나는 이 Text widget을 중복이 되는 가장 작은 widget이라고 생각하지도 않았다. 왜냐면 너무 흔했다. 그래서 string과 같은 primitive data type으로 생각했지, widget으로 생각지도 않았고, 중복되니까 custom widget으로 만들생각도 안했던 것이다. 이게 내가 잘못 생각했던 시행착오다. Text가 가장 작은 중복 widget이고, 이것 부터 만드는게 순서라는 것이다. (edited) [9:54 AM] Text는 여러 종류가 사용되고 중복이 되도 1-2번 많은것은 3-4번 되는 아주 흔히 사용되는 widget이기에 이것을 custom한다는 생각을 안할것이다. 나만 그런게 아닐것이다. 그런데 내가 시행 착오로부터 배운 결론은 Text같이 작은 중복 widget부터 custom하라!다. (edited)
hoyoul — 11/19/2023 10:08 AM Text widget을 어떻게 custom할 것인가? [10:10 AM] 위에서도 말했듯이 Text widget은 다양한 종류가 아주 적게 중복이 되면서 사용된다. 이것을 모두 custom widget으로 만들면, 장점은 해당 custom widget에 속성들을 정의했기 때문에 더 큰 widget에서 사용할때 코드가 간편해 보인다는 장점이 있다. 단점은 custom text widget파일들이 너무 많아진다는 것이다. [10:10 AM] 나는 모두 custom text widget으로 만드는것은 기분이 좋지 않았다. 왠지 꺼름직했다. [10:11 AM] 이것을 tdd에선 bad smell이라고 했나? 여튼 안좋다. [10:11 AM] 내가 선택한 방식은 factory pattern을 사용하자였다. factory pattern은 코드가 길어도 상관없다. [10:12 AM] 대신 자신을 희생하면서 다른 코드를 simple하고 가독성있게 만들어 줄 수 있기 때문이다. [10:12 AM] 그래서 다음과 같이 text를 만드는 공장?ㅋ을 만들었다. [10:13 AM] class TextFactory { static Text createText(String string, TextType type) { TextStyle textStyle;
switch (type) {
/ titleText case TextType.titleText: textStyle = const TextStyle( color: Colors.white, fontSize: 27, fontWeight: FontWeight.bold, ); break; / googleButtonText: case TextType.googleButtonText: textStyle = const TextStyle( color: Color(0xff272731), fontSize: 16, fontWeight: FontWeight.w600, ); break; [10:14 AM] 구조는 입력으로 두가지를 받는다. 하나는 원하는 string, 그리고 다른 하나는 text의 종류다. [10:14 AM] 이렇게 factory를 만들면, 가독성이 좋아진다. [10:14 AM] @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xff272731), body: SafeArea( child: Center( child: Column( children: [ TextFactory.createText(“actionNumber”, TextType.actionNumberText), TextFactory.createText(“actionText”, TextType.actionText), TextFactory.createText(“appleButtonText”, TextType.appleButtonText), TextFactory.createText(“googleButton”, TextType.googleButtonText), TextFactory.createText(“leftSeatNumberText”, TextType.leftSeatNumberText), TextFactory.createText(“playFlagText”, TextType.playerFlagText), TextFactory.createText(“potcallnumberText”, TextType.potcallNumberText), ], ), ), ), ); } }
hoyoul — 11/19/2023 10:15 AM 세번째, Text나 자주사용되는 roundbutton의 경우, Image도, enum과 factory패턴으로 구현한다. enum에서는 모든 text, roundbutton을 다 나열한다. 한번만 사용되더라도 다 나열한다. 그리고 switch case문에서 중복이 되는것은 공통의 case문으로 나타내면 된다. 이렇게 하면, 장점이 name을 유지하면서 공통의 코드를 갖는게 가능하다. 즉 사용할때는 제각기 주어진 이름을 사용하지만 동일한 코드를 갖는것이다. 예를들어서 join이라는 roundedbutton과 google이란 버튼은 동일한 style이다. 이것을 공통의 class로 만들면 각각의 이름이 사라진다. 10개의 rounded button이 동일한 style일때, general한 class로 만들어 사용하면, 어떤 것들이 어디에 속해 있는지 찾아야 한다. 그런데 enum을 사용하면 각각의 이름을 그대로 사용할 수 있다. enum TextType {gameGoogleSignInText, gameJoinText …}
/ gameGoogleSignInText / gameJoinText, case TextType.gameGoogleSignInText: case TextType.gameJoinText: textStyle = GoogleFonts.inter( textStyle: const TextStyle( color: Color(0xff272731), fontSize: 16, fontWeight: FontWeight.w600, ), ); break;
위에서, 두개의 이름은 하나의 case문을 공유한다. 따라서 이 factory를 사용할때, createText(“message string”,“gameGoogleSignInText”), createText(“message string”, “gameJoinText”),
이렇게 이름을 그대로 사용하기 때문에 가독성이 좋아지는 것이다.
ps: 주의 사항 모든 component를 factory를 사용할 수 없다. 특히 size정보를 명시해야하는 경우에는 factory를 사용하기 어렵다. 동적 device에 대응하기 위해서, mediaquery를 얻어와야 하는데, factory에는 buildContext()가 없기 때문이다. 가져오는 편법이 있지만, 가독성을 떨어뜨린다.
7개의 text를 사용하는데 7개의 문장이 쓰인다. 예전에는 1개의 text를 표현하는데, 5-7줄의 문장이 표현되었다. (edited) [10:16 AM] 50개의 줄대신 7개의 줄은 가독성을 높여준다. [10:17 AM] 아래는 수행결과다.

Figure 37: design1
—————— 오늘 해야 할거 ————–
hoyoul — 11/19/2023 10:26 AM
- Text widget사용하는 custom widget들의 변경
- 이름 변경 page, component로 변경
- fixed size에 대한 고민.
[10:26 AM]
- 피망섯다 설치하고 섯다게임 사용법 공부
- GetX controller의 사용 (edited)
hoyoul — 11/19/2023 10:42 AM jira에서 sprint가 오늘까지다. 아무래도 연장을 해야 할듯하다. [10:44 AM] 그런데 jira를 사용할줄 모른다. 동영상이라도 하나 봐야겠다. [10:45 AM] youtube검색하니 최상단에 https://www.youtube.com/watch?v=Vwa7BQ6WFYw 이게 나왔다. 한번보자.
대충 살펴봤다. 이건 나한테 필요한게 아니다. 배경설명하고 개괄인데…난 실질적 사용법을 원하기 때문에 pass [10:48 AM] https://www.youtube.com/watch?v=yZsiwCU-djQ
이게 뭔가 사용법을 설명하는듯하다.
hoyoul — 11/19/2023 11:06 AM 용어가 나온다. backlog, sprint, 우선 단어뜻을 알아보자. [11:06 AM] backlog: an accumulation of something, especially uncompleted work or matters that need to be dealt with. “the company took on extra staff to clear the backlog of work” (edited) [11:07 AM] sprint: (especially in software development) a set period of time during which specific tasks must be completed. (edited) [11:09 AM] backlog는 해야할 문제나 미구현, 미완성된 것들을 통칭하는듯 하다. 이런것들이 쌓아져 있는것들..원래 log는 통나무다. 뒷마당에 쌓인 통나무들…뭐 이런 느낌이다. [11:10 AM] 즉 땔감으로 사용될 통나무들은 아직 사용하지 않고 쌓아놓은것처럼 해야할 task를 모아놓은것…혹은 해야할 task라고 해석하면 될듯하다. [11:11 AM] sprint는 period..일정을 생각하면 될듯 하다.
hoyoul — 11/19/2023 11:13 AM issue라는 말도 한다. issue가 지라를 사용하는 이유라고 하는데…issue라는 단어는 jira에서 일종의 term인듯하다. [11:17 AM] issue는 여러종류가 있는데, epic,story,task,bug등이 있다고 한다. epic은 대주제라고 하는데, 그럼 project를 만들고, epic을 만들고 epic안에 story,task,bug등으로 소주제를 만든다는 얘긴가? [11:20 AM] 여튼, 실습으로 들어간다. 첫번째로 board를 설명한다.
hoyoul — 11/19/2023 11:21 AM board가 뭐지? 했더니, Jira에서 ‘보드(Board)‘는 작업을 시각적으로 관리하고 추적하는 데 사용되는 인터페이스를 말합니다. [11:22 AM] board하면 떠오르는 생각은 blackboard다. 칠판.ㅋ [11:22 AM] 아니면 surfing board처럼…판때기. [11:22 AM] 시각화해서 보여줄려면 board가 필요하겠지…이런생각을 한다. [11:23 AM] 보드에는 kanban board, scrum board, dash board등이 있다고 한다. [11:23 AM] 여튼, 보니까 나도 board가 보인다.
hoyoul — 11/19/2023 11:30 AM 근데 세미나라서 board설명하다가 갑자기 sprint로 넘어간다. [11:31 AM] 여튼 sprint를 설명하는데, sprint는 2-4주에 해당하고, sprint가 정해지면, 그것에 해당하는 세부 task들을 개발자들이 채워야 한다고 한다. [11:34 AM] 다시 board로 돌아와서 board에는 sprint들이 보이는데, 내경우는 todo, in progress, done이 되어 있다. todo에서 내가 세부작업을 만들어서 추가해도 된다. 근데 내경우는 task가 주어지니까 그냥 진행중이면 in progress로 옮기면 될듯하다. [11:36 AM] 세부로 나눠도 되고 안해도 되고…여튼 진행중일때는 in progress로 옮기면 된다고 한다. 아니다 세부 issue들은 개발자들이 만들어야 하는게 맞는듯하다. 왜냐면 하위 issue라는 메뉴가 있기 때문에, 거기에 세부 issue들을 만들면 되는듯하다. add a child issue가 있다. (edited)
hoyoul — 11/19/2023 11:38 AM 강의에서는 in test란 sprint가 하나 더 있다. 강의를 하는 팀에선, q&a를 거기서 한다고 한다. 기획팀에서 in test에 올라온 걸 보고 빠꾸 시킬수도 있고…댓글로 의견달고 그런다고 한다. 우리는 그 과정은 필요없으니 생략한거 같다.
hoyoul — 11/19/2023 11:48 AM 이 강의에서 재밌는 부분이 나오는데, git workflow다. [11:50 AM] 물론 git pro책에도 있고, 아는 내용이긴 한데, 여튼 jira에서 git을 연동하는 법을 workflow와 함께 설명한다.
hoyoul — 11/19/2023 3:19 PM git과 slack을 연동할수도 있는데, 뭐 그렇다. 이정도만 알아도 쓰는데는 문제 없을듯하다. [3:23 PM] 그냥 용어만 알면 될듯하다. backlog는 해야할일이고, issue가 backlog에 해당하는 구체적인것들인데, epic도 있고, story도 있고…즉 pm이 project만들고, task도 기획팀하고 같이 만들어서 디자이너나 개발자 다른 기획자들에게 던저주면 그 큰틀에서 issue를 개발자가 만들어도 되고, pm이 추가해도 되고…여러 issue들이 만들어지면 그걸 작업하고 작업 진행정도에 따라 in progress done으로 넘어가는거로 보면된다. 물론 여기에 git과 slack이 연동되서 더 자세한 정보를 보여줄수도 있고..그런거 같다. 여튼 대충은 알아봤고 이정도면 사용하는데 문제는 없을꺼 같다. (edited)
hoyoul — 11/19/2023 3:53 PM
- Text widget사용하는 custom widget들의 변경
mac ventura update 시 flutter doctor 멈추고 build 안되는 문제 발생. => sonoma 14.11 로 재설치 문제없이 동작됨.
11.20
ui component 수정(text factory이용, mediaquery추가, renaming) [9:11 AM] generalRoundedButton component, logoRoundedButton component
hoyoul — 11/20/2023 9:22 AM /* screen size: 375x812
container size info: 327x56 width: MediaQuery.of(context).size.width * 0.872, height: MediaQuery.of(context).size.height * 0.068,
icon size: 45x38 width: MediaQuery.of(context).size.width * 0.12, height: MediaQuery.of(context).size.height * 0.046, */ import ‘package:flutter/material.dart’;
class LogoRoundedButtonComponent extends StatelessWidget { final bool isGoogle; final Text brandText;
const LogoRoundedButtonComponent({ Key? key, required this.brandText, required this.isGoogle, }) : super(key: key);
Color _getBackgroundColor(bool isGoogle) { return isGoogle ? const Color(0xffffffff) : const Color(0xff000000); }
String _getImagePath(bool isGoogle) { return isGoogle ? “lib/images/google.png” : “lib/images/apple.png”; }
@override Widget build(BuildContext context) { return GestureDetector( onTap: () { print(‘pressed’); }, child: Container( width: MediaQuery.of(context).size.width * 0.872, height: MediaQuery.of(context).size.height * 0.068, / color: _getBackgroundColor(isGoogle), decoration: BoxDecoration( color: _getBackgroundColor(isGoogle), / 색상을 설정. borderRadius: BorderRadius.circular(12), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, / 가로 정렬. crossAxisAlignment: CrossAxisAlignment.center, / 세로 정렬. children: [ Image.asset( _getImagePath(isGoogle), width: MediaQuery.of(context).size.width * 0.12, height: MediaQuery.of(context).size.height * 0.046, ), brandText, ], ), ), ); } } [9:22 AM] size를 계산해서 media query 설정
hoyoul — 11/20/2023 9:49 AM rounded button이 너무 많다. 그냥 factory로 만들자. [9:51 AM] logo도 어차피 둥근 버튼이고, action도 둥근 버튼이고 exit profile money 모두 둥근버튼이다. 따로 따로 만들 필요가 없다.
hoyoul — 11/20/2023 10:36 AM 상단에 있는 정보란게 player에 대한 정보인지, game에 대한 정보인지..보기엔 player에 대한 정보같은데… [10:42 AM] naming을 선택하는게 중요한데, 그냥 추론해서 이름을 짓다보니 엉망이란걸 알았다. [10:42 AM] 좀 더 체계적으로 이름을 져야 하는데… [10:43 AM] game, player, opponents라는 틀에서 이름을 짓기로 했다.
hoyoul — 11/20/2023 10:43 AM game에 관련한건 game, player에 관련한건 player, 다른 사람들을 other player로 했었는데, 그것보다 oppnent가 더 낫다. prefix로 해야할 듯하다.
hoyoul — 11/20/2023 10:59 AM enum TextType { playerSeedMoneyText, / ex) number on pot or call on the top “pot 3000” playerSeedText, / ex) pot, call playerActionMoneyText, / ex) number on action text like “30 Die” playerActionText, / ex) Die,Call
opponentFlagText, / ex) 1st, win opponentNameText, / ex) user1, user2
gameTitleSignInText, / ex) “Log in or Sign up”, “2 Card Play” gameLogoGoogleText, / ex) “google” “join” gameLogoAappleText, / ex) “apple” gameWaitingNumberText, / ex) 3 in “3 other player waiting” gameLeftSeatNumberText, / ex) 3 in “3 seat left” gameWaitingText, / ex) except 3 in “3 other plyaer waiting” gameLeftSeatText, // ex) except 3 in “3 seat left” } [11:00 AM] 이전보다 조금 가독성이 있어보인다.
hoyoul — 11/20/2023 11:24 AM font를 보면 size정보에 baseline을 포함하면, 14, 아닌건 10 이런게 있다. 나는 baseline을 적용하지 않은 size를 사용한다. 대부분 이렇게 한다고 한다.
hoyoul — 11/20/2023 11:40 AM factory를 설계할때, text가 되었건, round button이 되었건 enum에는 모든 type을 다 써준다. 그러면 중복된 거 처리를 안하는거냐?라고 물어볼 수 있다. 그렇지 않다. switch case에서 동일한 case는 동일하게 처리한다. 다만 enum에서만 구분 할 뿐이다. [11:41 AM] 예를 들어서 join버튼과 google 버튼은 동일한 모양이고 글씨만 다르다. 이걸 하나의 공통class로 만들수도 있지만, [11:45 AM] 그렇게 하면 가독성이 떨어진다. generalButton이라고 하고 join과 google을 상속해서 쓸때, generalbutton에는 뭐가 있었지, 찾아야 한다. 그리고 join과 google은 연관성도 없다. 반면 enum과 switch를 사용하면 이름은 그대로 enum에 정의해서, joinButton,googleSignInButton 이라고 사용한다. code만 동일하다. (edited) [11:46 AM] case에서 두개의 속성을 갖게 만들면 된다. [11:46 AM] 그러면 이름도 유지하고 속성은 공유하게 된다.
hoyoul — 11/20/2023 11:52 AM / gameGoogleSignInText / gameJoinText, case TextType.gameGoogleSignInText: case TextType.gameJoinText: textStyle = GoogleFonts.inter( textStyle: const TextStyle( color: Color(0xff272731), fontSize: 16, fontWeight: FontWeight.w600, ), ); break; (edited) [11:52 AM] 이런식이다.
hoyoul — 11/20/2023 12:00 PM text factory를 더 간결하게 만들고 싶지만, fontsize와 fontweight와 color가 각각 다르기 때문에 공통으로 무언가를 뽑아내기가 쉽지 않다. 어차피 factory는 다른 코드의 가독성을 위해 희생한다는 패턴으로 이해하고 넘어가야 한다.
hoyoul — 11/20/2023 12:59 PM rounded button factory
hoyoul — 11/20/2023 2:07 PM container를 factory해서 가져올려고 할때 문제가 있다. container의 width와 height는 context에서 가져오는데, factory는 build함수가 없다. 즉 mediaQuery를 가져올 수 없다. device정보를 못가져오면 width와 height가 동적으로 setting될 수 없다. [2:08 PM] widget tree가 만들어진 후 rendering이 되는 widget만이 device정보를 가져올 수 있는것인가?
hoyoul — 11/20/2023 2:15 PM 가져올 순 있지만, 가독성을 떨어뜨린다. 왜냐하면 widget tree와 context의 개념을 알고있다면 왜 이렇게 했는지 유추할수 있지만, 정상적이지 않기 때문에 혼란을 일으킨다. [2:16 PM] 정상적인건, context가 참조되는곳에서 device정보를 가져오는게 자연스럽기 때문이다. [2:16 PM] 그렇게 쓰니까…그런데 갑자기 factory에서 context를 외부에서 정의하고 참조해 쓴다면…혼란 스러워진다. 이렇게 해선 안된다. [2:18 PM] 그러면 어떤 방식이 있을까? [2:19 PM] General한 stateless widget을 작성하는 수밖에 없다.
hoyoul — 11/20/2023 2:23 PM 다시 재작성해야한다. [2:26 PM] /* screen size: 375x812
-
gameAppleSignInButton, gameGoogleSignInButton, gameJoinButton size info: 327x56 width: MediaQuery.of(context).size.width * 0.872, height: MediaQuery.of(context).size.height * 0.068,
-
gameExitButton, playerProfilebutton size info: 32x32 width: MediaQuery.of(context).size.width * 0.039, height: MediaQuery.of(context).size.height * 0.085,
-
playerActionButton size info: 92x45 width: MediaQuery.of(context).size.width * 0.245, height: MediaQuery.of(context).size.height * 0.055,
-
opponentbettingmoneybutton size info: 59x28 width: MediaQuery.of(context).size.width * 0.157, height: MediaQuery.of(context).size.height * 0.034,
ps: code가 좀 복잡하다. 공통된 속성인 curved border를 따로뺐다. color는 container에서 직접 정의가 안된다. border에 color가 정의되기 때문에, color를 가져오는 inner function을 내부에 사용해야 하는데, inner function을 사용하지 않기 위해서 stataic function을 썼다. [2:27 PM] import ‘package:flutter/material.dart’;
enum RoundedButtonType { gameAppleSignInButton, / ex) “continue with apple button” gameGoogleSignInButton, / ex) “continue with google button” gameJoinButton, / ex) “join” gameExitButton, / ex) exitbutton
playerActionButton, / ex) “Die,Double” playerProfileButton, / ex) profile ic
opponentBettingMoneyButton, // ex) “30,000” }
class RoundedButtonFactory { static Color getBackgroundColor(RoundedButtonType type) { switch (type) { case RoundedButtonType.gameGoogleSignInButton: case RoundedButtonType.gameJoinButton: return const Color(0xffffffff); case RoundedButtonType.gameAppleSignInButton: return const Color(0xff000000); case RoundedButtonType.playerProfileButton: case RoundedButtonType.gameExitButton: return const Color(0xff11111b); case RoundedButtonType.playerActionButton: return const Color(0xff13131d); case RoundedButtonType.opponentBettingMoneyButton: return const Color(0xff0a0a0e); default: return Colors.transparent; } }
static String getImagePath(RoundedButtonType type) { switch (type) { case RoundedButtonType.gameGoogleSignInButton: return “lib/images/google.png”; case RoundedButtonType.gameAppleSignInButton: return “lib/images/apple.png”; case RoundedButtonType.gameExitButton: return “lib/images/exit.png”; default: return “lib/images/exit.png”; } } [2:27 PM] static Container createRoundedButton(String string, RoundedButtonType type) { // 공통된 border, but color는 border에서만 정의된다. BoxDecoration buttonDecoration(RoundedButtonType type) { return BoxDecoration( borderRadius: BorderRadius.circular(12), color: getBackgroundColor(type), //color는 공통적이지 않아서 따로 뺐다. ); }
switch (type) { case RoundedButtonType.gameAppleSignInButton: return Container( width: 150, height: 50, decoration: buttonDecoration(type),
); case RoundedButtonType.gameGoogleSignInButton: return Container( width: 120, height: 40, decoration: buttonDecoration(type),
); case RoundedButtonType.gameJoinButton: return Container( width: 100, height: 30, decoration: buttonDecoration(type),
); /다른것도 똑같이 / …
default:
return Container();
}
} } [2:28 PM] factory 코드라 길다. 이것을 General한 stateless widget으로 만들어야 한다.
hoyoul — 11/20/2023 2:37 PM stateless widget을 사용하면 etX를 사용할때 ubx가 지저분해질 수 있는데…이건 해봐야 알듯하다.
hoyoul — 11/20/2023 3:29 PM widget으로 general하게 짜면 불가피하게 if나 switch문이 사용될 수 밖에 없는데, 이걸 getX로 뽑아내기가 힘들어 보이긴 하다. 그러면 간단하고 직관적이진 않을 수도 있다. [3:30 PM] 처리를 어떻게 하냐?에 달려있긴 한데…
hoyoul — 11/20/2023 3:43 PM 하나의 widget으로 다 처리하면 코드가 복잡해질 우려가 있다. 3종류의 widget으로 나눠서 처리해야겠다. logo+ text의 roundedbutton, text rounded button, logo rounded button 이렇게 하는게 나을듯하다.

Figure 38: design2

Figure 39: design3
코드는 단순해졌고, 직관적으로 바뀌긴 했다. [4:57 PM] 종류에 관계없이 일관된형태로 사용할 수 있게 되었다. [4:59 PM] naming이 좀 길다. 난 원래 짧은 naming을 선호했다. 그런데 길어도 가독성있는걸 써야 한다는 글을 읽고 바꿨는데 모르겠다. [5:00 PM] 그리고 device별로 문제가 없는지…확인해봐야 한다.
hoyoul — 11/20/2023 5:04 PM 문제있다.

Figure 40: design3
media query가 적요되는걸 확인했는데, 리팩토링시에 뭐가 꼬인 모양이다.
hoyoul — 11/20/2023 6:19 PM => 해결했다. image의 width와 height를 설정했는데, image size + text size가 overflow되서 발생한다. 이것은 비율을 계산하면 소숫점으로 나오는데 이를 정확하게 나타낼수 없고, 그냥 반올림하는데..여기서 곱한 결과가 실제 size보다 조금이라도 크면 overflow가 난다. 해결은 image width와 height를 주지 않았다. 왜냐면 container에선 자동으로 줄여지거나 늘어나기때문에 처리하는 부분을 뺐다. children: [ Image.asset( _getImagePath(roundedButtonType), / width: _getWidth(context,roundedButtonType), / height: _getHeight(context,roundedButtonType), ), text, [6:23 PM] 작은 device에서도 테스트해보자.

Figure 41: design5
이상없이 되긴하다.

Figure 42: design6
icon size차이는 얼마 안난다. - apple image size: 45x38 google image size: 44x45 [6:49 PM] 이 정도의 크기는 같다고 처리해도 무방한데…왜 크기 차이가 날까? [6:50 PM] height가 주 원인인데, 7정도의 차이인데…이게 비율로 해봤자..2pixel.. [6:50 PM] 그정도인데…

Figure 43: design7
동일한 사이즈를 구글링해서 다운받고 적용해보았다. [6:58 PM] 해상도도 동일한걸로 했다. [6:59 PM] 글자와 로고사이의 간격만 8정도 넣으면 될듯하다.
hoyoul — 11/20/2023 7:41 PM how to use custom round button (edited) [7:43 PM] const SizedBox(height:30), TextFactory.createText(“custom Roundbutton”, TextType.gameLoginTitleText), const SizedBox(height:50), TextIconRoundedButtonComponent( text: TextFactory.createText( “Continue with Google”, TextType.gameGoogleSignInText), roundedButtonType: TextIconRoundedButtonType.gameGoogleSignInButton, ), const SizedBox(height:15), TextIconRoundedButtonComponent( text: TextFactory.createText( “Continue with Apple”, TextType.gameAappleSignInText), roundedButtonType: TextIconRoundedButtonType.gameAppleSignInButton, ), const SizedBox(height:10), const IconRoundedButtonComponent(iconRoundedButtonType: IconRoundedButtonType.gameExitButton), const SizedBox(height:10), TextRoundedButtonComponent( text: TextFactory.createText( “join”, TextType.gameJoinText), textRoundedButtonType: TextRoundedButtonType.gameJoinButton, ), const SizedBox(height:10),
[7:43 PM] const SizedBox(height:10), TextRoundedButtonComponent( text: TextFactory.createText( “30,000”, TextType.opponentBettingMoneyText), textRoundedButtonType: TextRoundedButtonType.opponentBettingMoneyButton, ), const SizedBox(height:10), TextRoundedButtonComponent( text: TextFactory.createText( “Die”, TextType.playerActionText), textRoundedButtonType: TextRoundedButtonType.playerActionButton), const SizedBox(height:10), TextRoundedButtonComponent( text: TextFactory.createText( “Double”, TextType.playerActionText), textRoundedButtonType: TextRoundedButtonType.playerActionButton), const SizedBox(height:10), TextRoundedButtonComponent( text: TextFactory.createText( “Call”, TextType.playerActionText), textRoundedButtonType: TextRoundedButtonType.playerActionButton), ], [7:44 PM] 결과는 다음과 같다.

Figure 44: design8
그냥 가져다가 쓰면 된다.
hoyoul — 11/20/2023 10:33 PM http서버에서 player1,..5라는 client와 connection을 유지하는 방법은 session이다. [10:34 PM] session이란게 http가 connection객체를 끊어버리는 문제를 해결할려고 나온개념이기 때문이다. [10:34 PM] 그렇다면 session을 이용해서 player1-5와 개별적 통신이 가능하지 않을까?
11.22
login 과정 처리 [8:52 AM] 게임과 관련한 ui component들은 미완성이지만, login관련 2개의 page를 구성하는ui component들은 있기 때문에 2개의 page를 만들어 보자.

Figure 45: design9
새로운 디자인이 추가되서 두가지의 디자인이 있다. 우선 첫번째 안으로 하자. 두번째로 바뀌더라도 text가 textfactory에 있다면 가져다 쓰면 되고, 없으면 등록하면된다. image도 factory형태로 만들어서 갖다쓰고 등록해서 하는게 편할지도 모른다. image도 text와 마찬가지로 다양한 종류가 쓰이기 때문에, 화투패를 생각하면 text와 동일한 형태로, 즉 factory로 처리하는게 맞는거 아닌가 하는 생각이 든다. 그렇게 하면 page layout이 단순해 지니까… 뭐 이런 저런 가능성이 있는데. 여튼 첫번째로 우선 해보자. [9:05 AM] 첫번째 page (edited) [9:06 AM] 3개의 component가 사용되는데 그냥 가져다가 붙이면 된다.
hoyoul — 11/22/2023 9:07 AM Login이라는 Text는 text factory에서, 2개의 버튼은 rounded button에서.. [9:10 AM] rounded button은 모두 container로 작성했다. button자체는 button클래스들이 많이 있다. elevated 버튼과 같은것을 수정해서 만들수도 있지만, 난 둥근것에 초점을 뒀다. 그래서 container에 gesture detector를 달면 버튼과 같은거 아냐? 라는 단순한 생각으로 container로 통일 시켰다. 이게 맞는지 안맞는지는 모르겠다. 즉 tap과 press에 반응하는것들, action과 logo를 button으로 처리하는것과 container로 처리하는거에 차이는 있을수 있다. 난 다 똑같다고 생각했는데…모르겠다. [9:13 AM] 즉, button과 container로 나누는게 맞는거 아니냐?라고 물어본다면, 그치…그게 맞는데, 작성 당시에는 버튼같이 둥근것들은 하나의 클래스나 팩토리로 통일하고 싶었던 생각이 너무 강하게 들어서…뭐가 맞는지 틀리는지는 모르겠다. 우선 진행하자. [9:14 AM] naming을 뭐라고 할것인가?
hoyoul — 11/22/2023 9:19 AM naming [9:22 AM] 이건 LoginPage, SignInPage해도 상관은 없을듯 하다. 어떤걸로 해도 명확히 알수 있으니..그냥 LoginPage로 만들자. [9:23 AM] login page작성 [9:25 AM] emacs에서 import를 하면 경로를 자동완성하는 기능은 없을까?
hoyoul — 11/22/2023 9:27 AM https://github.com/Dart-Code/Dart-Code/issues/3211
2021년도에 뭔가 있었는데, 해답은 없다. [9:27 AM] 그냥 쓰자.

Figure 46: design10
3개의 component가 있다. 1개의 text, 2개의 roundedbutton [9:39 AM] 이미 만든 compoent를 사용하자. [9:40 AM] page를 호출할때 3개의 compoent를 전달할것인가? 아니면 page를 그대로 호출할 것인가? [9:41 AM] page를 그냥 호출하자. 외부에서 인자로 전달하는 식으로 하자. [9:42 AM] 왜냐고? 방법의 차이를 모르니까, 그리고 page는 어떻게 보면 compoent를 조합해서 사용하는 최종 page니까…
hoyoul — 11/22/2023 9:47 AM import ‘package:flutter/material.dart’; import ‘package:kholdem_v1/components/rounded_button_components/text_icon_rounded_button_component.dart’; import ‘package:kholdem_v1/components/text_component_factory/text_factory.dart’;
class LoginPage extends StatelessWidget {
const LoginPage({super.key});
@override Widget build(BuildContext context) { return Scaffold( body: SafeArea( child: Center( child: Column( children: [ TextFactory.createText(“custom Text”, TextType.gameLoginTitleText), TextIconRoundedButtonComponent( text: TextFactory.createText( “Continue with Google”, TextType.gameGoogleSignInText), roundedButtonType: TextIconRoundedButtonType.gameGoogleSignInButton, ), TextIconRoundedButtonComponent( text: TextFactory.createText( “Continue with Apple”, TextType.gameAppleSignInText), roundedButtonType: TextIconRoundedButtonType.gameAppleSignInButton, ), ], ), ), ), ); } } [9:47 AM] 딱 3개만 사용했다. [9:47 AM] 이제 layout을 만들자.

Figure 47: design11
디자인하고 비슷하게 나왔다. 그런데 내가 layout을 사용할때 공간을 sizedbox를 쓰는데 여기에 사용되는 height는 mediaquery를 적용하지 않은 것이다. [9:56 AM] media query를 적용하자.
hoyoul — 11/22/2023 10:08 AM media query를 위해 매번 수동으로 비율을 계산하는건 아닌거 같다. [10:09 AM] screen size가 375x812다. 그리고 figma에서 height가 20정도라면 [10:13 AM] double screenHeight=812; double getDeviceWidth(double height){ return Double((height/screenHeight).toStringAsFixed(3)); } [10:14 AM] 이런식의 코드를 만들면 어떨까? 소숫점 3자리만 유지해서 return할려고 하는데 찾아보니 string으로 변환한다음에 다시 double로 하라고 하네..이건 아닌거 같은데…dart api에서 좀 찾아보자.
hoyoul — 11/22/2023 10:19 AM 소숫점이 영어로 뭔지 모르겠다. trimming으로 검색을 했는데… [10:20 AM] decimal point라고 한다. [10:20 AM] decimal point below trimming in dart…콩글리시다.ㅋ [10:22 AM] double d = 2.3456789; String inString = d.toStringAsFixed(2); / ‘2.35’ double inDouble = double.parse(inString); / 2.35 [10:23 AM] stack overflow에서 이렇게 쓴다고 나왔다..좀 아니지 않나? 왜 string을 다시 double로 고치지..double에서 지원하는게 맞는데… [10:24 AM] 10분만 쉬고, mediaquery변환함수 만들고, 두번째 page도 만들자.
hoyoul — 11/22/2023 10:35 AM utils라는 폴더를 만들었다. 그리고 변환함수를 작성하기로 했다. [10:40 AM] class DeviceUtils { static double getConvertedWidth(BuildContext context, double width) { double realWidth = width/MediaQuery.of(context).size.width; String inString = realWidth.toStringAsFixed(3); / ‘2.351’ return double.parse(inString); / 2.351 } static double getConvertedHeight(BuildContext context, double height) { double realHeight = height/MediaQuery.of(context).size.width; String inString = realHeight.toStringAsFixed(3); / ‘2.351’ return double.parse(inString); / 2.351 } }
hoyoul — 11/22/2023 10:43 AM 이렇게 만들었다. 우선 이전에 container의 width와 height를 factory로 만들수 없었던게 build context를 가져와서 사용할 수 없었기 때문에 roundedbutton을 factory로 못만들고 class로 만들었다. 그런데 이경우 context를 인자로 넣어서 계산할 수 있기 때문에 그렇게 했다. roundedbutton에선 context를 전역으로 선언하고 가져오는 방식이 되어버리기 때문에 가독성때문에 포기했지만, 간단한 함수는 이렇게 context를 전달하고 계산된 값을 return받는 형태로 쓰는건 무리가 없다. [10:45 AM] 사용할때는 그냥 아래처럼 사용하면 된다. width: DeviceUtils.getConvertedWidth(context, height); //figma에서 가져온 height를 넣어준다. 그러면 비율을 return하는데, 소수점 3자리의 값이다.
hoyoul — 11/22/2023 10:53 AM 헉 안된다.

Figure 48: design12
디버거로 확인하면 계산도 된다. 그런데, 이렇게 함수 처리를 해서 context로 얻어오는 작업은 dynamic한 작업이다. 즉 runtime때 context에 접근해서 device값을 얻어와서 처리가 된다는 것이다. 예를 들어서 const sizedbox(height:30)은 const로 처리한다. compile때…그래서 동작이 되는데, sizedbox(height:DeviceUtil(context,44)는 const를 사용하지 못한다. runtime때 실행되기 때문이다. [10:57 AM] 그럼 어떻게 해결할 수 있는가? 두가지 방법이 떠오른다. 하나는 미리 계산을 하고 그 값을 사용하는 방법이 있다. 두번째는 이전과 같이 미리 계산된 값을 넣는것이다. [10:57 AM] 첫번째 방식은 변수를 만들어야 한다. 그리고 그 변수를 const SizedBox(height: heighvalue), 이렇게 사용하면 되고, [10:58 AM] 두번째 방식은 const SizedBox(height: 3.23) 라고 미리 계산된 값을 넣어주는 것이다. [10:58 AM] 난 가독성이 떨어지지만 일관성을 위해서 두번째를 선택하겠다.
hoyoul — 11/22/2023 11:00 AM 아니다…일관성은 계속 변수를 사용하는 식으로 해결되지만, 가독성은…해결되지 않는다. 소숫점이 코드에 보이면 가독성은 떨어진다. 알수 없는 숫자가 코드에 있어선 안된다. 상수로 알기쉽게 처리하는게 맞다. [11:00 AM] 점심먹기전까지 2개의 page를 만들고 오후에 model작업을 하자.
hoyoul — 11/22/2023 11:07 AM 아니다. 변수가 그러면 stateless widget에 너무 많아진다. 그냥 비율 literal을 때려박는게 맞다. [11:08 AM] 여백도 width,height를 갖고 공간은 어차피 size로 세팅해야 하는데 이모든걸 변수처리하는건 아니다. [11:12 AM] height: MediaQuery.of(context).size.height* 0.024, [11:13 AM] 어차피 가독성에 큰차이 없다. figma에서 height가 20이면 비율을 20/875(screen height)로 나눈값을 수동으로 계산해서 곱해줘도 가독성에 별차이 없다.
hoyoul — 11/22/2023 11:16 AM 동작은 된다. 근데 좀 이상하다. 여기서 media query도 context를 가져와서 하면 runtime인데…인자로 보내서 계산하나 똑같잖아… [11:16 AM] 좀 이상하다. 그냥 넘어가자. 여튼 일관성있게만 하자. [11:16 AM] 두번째 page (edited) [11:19 AM] naming [11:20 AM] 이름은 자연님이 하신 intro page도 좋고 gameRoomSelection Page도 좋고, GameLobby page도 좋다. [11:20 AM] GameIntroPage로 하자.

Figure 49: design13
이것도 3개의 component가 들어간다. 만들어놓은게 있으니 그냥 가져다 쓰면 된다. [11:22 AM] layout은 고려하지 않고 가져다 써보자.

Figure 50: design14
코드는 다음과 같다. [11:31 AM] children: [ TextFactory.createText(‘2 Card Play’, TextType.game2CardText),
CircleComponent(
seatNumber: TextFactory.createText('2', TextType.gameLeftSeatNumberText),
seatText: TextFactory.createText('seat left', TextType.gameLeftSeatText),
waitingNumber: TextFactory.createText('3', TextType.gameWaitingNumberText),
waitingText: TextFactory.createText('other players waiting', TextType.gameWaitingText),
),
TextRoundedButtonComponent(
text: TextFactory.createText(" Join ", TextType.gameJoinText),
textRoundedButtonType: TextRoundedButtonType.gameJoinButton),
]
),
(edited) [11:32 AM] 3개의 code로 되어있다. 내가 만든 custom widget들은 문서를 만들어야 한다. 코드와 사용방법을 명시하면, 그냥 가져다가 쓰면 된다. [11:33 AM] layout [11:33 AM] layout은 sizedbox로 공간만 만들껀데, mediaquery를 만들어야 한다. [11:34 AM] 10분간 쉬고 하자.

Figure 51: design15
시안과 다른게 보이는데, 우선 두 화면은 다른 device에서 적용한거라 그렇다. 글자크기가 다르다. 글자크기는 변화가 있었던건 아닐텐데…좀 살펴봐야 하고…원 사이즈도 달라보이는데 이건 미디어 쿼리를 적용했기 때문이다. 즉 문제될께 없다. (edited) [11:57 AM] 글자 크기는 27에 weight는 900으로 했다. black이기 때문이다.
hoyoul — 11/22/2023 11:59 AM 디자인이 뭔가 바뀌었다. 우선 font정보가 안나온다. [11:59 AM] 회의때 style을 만들어서 추가한다고 했던게 적용된듯하다. [11:59 AM] 근데 난 figma의 style을 적용하는게 뭔지 모른다. [12:00 PM] pretty flutter라는 plugin이 안되는 이유가 figma에 style이 적용되지 않으면 안된다는건 아는데, 정작 figma에서 style을 볼줄 모른다.ㅋ [12:01 PM] 아… very pretty flutter plugin이 적용된다. [12:05 PM] 복사해서 보니, 다시 font정보가 보인다. [12:05 PM] 32로 변경되었다. [12:05 PM] 그리고 black에서 bold로 바뀌었다. 적용해 보자.

Figure 52: design16
얼추 비슷해졌다. [12:15 PM] test로 mediaquery를 iphon15 plus 디바이스에 해보자.

Figure 53: design17

Figure 54: design18
rotate도 해봤다. [12:18 PM] 당연히 비율에 따라 하기 때문에 문제가 있다. 지금현재는 portrait기준이라서 landscape은 신경쓰지 않는다. 뭐 신경쓰자면, 별도의 page를 만들수도 있는것이고, 새로운 비율로 만들수도 있는것이고…게임에서가 문제이지..intro page는 지금 신경안써도 된다.
hoyoul — 11/22/2023 12:24 PM 점심먹고 모델과 로긴 작업을 하자.
hoyoul — 11/22/2023 12:32 PM 음….직장생활하면서 절대 하지말아야할께 남의 코드에대한 평가다. 이건 한국직장에서 어떻게 보면 불문율에 가깝다. 100% 싸움나는 행동이라서 하면 안된다. 자연님코드가 이상하다는 확신이 있기때문에 언급을 했지만, 절대 하면 안된다고 스스로 다짐한다. 코드에 대한 내용은 보통 오프라인 코드리뷰시간에서 말한다. 자연님의 코드가 이상한건 getX를 사용하면서 stateful을 섞어서 사용한다. 이건 마치 rails에서 독자적으로 route파일을 만들고 여기에 mvc를 합한 코드를 사용하는것과 비슷하다. 동작은 문제없다. 함수를 안쓰고 class를 안쓰고 하나의 함수안에 1000개의 statement로 코드를 써도 동작은 된다. 우리는 이럴때 이상하다고 한다.하지만 문서나 주석으로 이유를 썼다면 용서가 된다. 그런데 난 주어진 이상한 코드에 대한 이해를 온전히 후임자의 몫으로 하는건 아니라고 본다. 내가 아니라, 내가 다른 사람에게도 그럴순 없는것이다.
hoyoul — 11/22/2023 1:32 PM 나는 getX가 너무나 자연스러운 package라고 생각한다. 왜냐면, 내가 flutter를 사용하면서 제일 이상했던게 statefull widget에서 상태data를 관리한다는 것이기 때문이다. [1:36 PM] 보통은 mvc모델을 분리해서 처리한다. swing에선 m과 c가 합쳐지고 view만 separate해서 작업한다. 다른것도 마찬가지다. windows는 모르겠지만, linux에선 kde나 gnome에서도 mvc로 구현되었다. small talk에서 이미나온 개념이다. 그리고 xwindow도 마찬가지였다. 내가 lg에서 wayland compositor를 구현했었는데, wayland의 widget들도 마찬가지였다. [1:37 PM] wayland는 xwindow의 개선판이다.
</img/oauth/wayland_getx.mp4>
여튼 getX는 controller를 제공하고, view에서 business logic을 다 제거한다. [1:43 PM] controller와 model을 합쳐서 쓰면 swing과 같은 구조가 되는것이고, controller와 model을 분리하면 smalltalk이나, wayland와 같은 구조가 되는것이다. 나는 3개를 분리하는게 맞다고 생각한다. 의존성을 없애고 독립적으로 유지하는게 재사용측면에서 장점이 있기 때문이다. [1:44 PM] login model작성 (edited) [1:46 PM] model은 user라는 class를 만들고 여기에 property를 기술할 것이다.
hoyoul — 11/22/2023 1:50 PM setter와 getter로 처리하고, getXcontroller를 상속하는 controller를 만든다. [1:50 PM] controller에서 obs를 만들어야 할까 아니면 model에서 해야할까? 이게 고민이 되는 부분이다. [1:51 PM] controller와 model을 합쳐서 사용한다면, controller에서 상태데이터를 정하고 obs를 건다. [1:52 PM] 근데 독립해서 한다면, controller에서 해야할지 모델에서 해야할지 고민이 된다. [1:54 PM] 이전에 작성했던걸 좀 읽어보고 생각을 정리할 필요가 있다.
(1) google_Signed_in package 설치 [2:11 PM] flutter pub add google_sign_in 이렇게하면 설치가 된다. [2:12 PM] model과 controller를 설계하기전 필요한 package다. 이게 있어야 oauth 통신이 가능하다. [2:14 PM] package의 사용법이 있는데, 예전에 해석하다 말았다. 지금 해석할 수 있기 때문에 구현해서 test를 해보자. 동작이 되면, 그 다음 getX model과 controller로 구현해야 한다. (edited) [2:15 PM] (2) google signed in package test [2:16 PM] 예제는 stateful로 되어 있다.
hoyoul — 11/22/2023 2:33 PM 우선 제일 궁금한건, 접속을 어떻게 하느냐?가 궁금하다. googleSignedIn package를 설치했으니까..어떤 api가 있어서 주소에 접속을 할꺼라고 생각한다. [2:33 PM] 자연씨 소스를 참조해보자.
hoyoul — 11/22/2023 2:44 PM await SettingController.to.handleGoogleSignIn(); 여기서 접속을 하는거 같다. bp를 걸고 debugger로 체크해보자. [2:46 PM] sharedpreference를 사용하기 때문에 button을 눌러서 googleSignIn()를 호출하는 부분을 건너 뛴다. [2:47 PM] shared preference란건, 폰에 있는 storage인데, 로긴정보를 저장하고 있다. 따라서 [2:48 PM] 만일 로긴 성공한 적이 있다면 다시 google에서 인증할 필요가 없기 때문에 해당 shared preference값을 꺼내서 rails 서버와 통신하는 메커니즘이다. [2:48 PM] 그러면 어떻게 해줘야 break point가 걸리게 할 수 있을까? [2:49 PM] google login버튼 화면 자체는 우리가 만드는거 아니였나? [2:49 PM] 다시 해보자.

Figure 56: getx3
이게 예전화면이였다.
hoyoul — 11/22/2023 2:52 PM 그러면 google 버튼 누르면 bp 걸리지 않을까…근데 이 화면이 나오고 로긴되는데, 이부분도 원래는 나오면 안된다. 왜냐 자동으로 게임화면으로 넘어가는데, 버튼도 누르지 않을 화면을 보여주는건 이상한거 아닌가? 지금 debugger를 쓰니까. step별로 동작을 확인할수 있어서 보이는건데. 이화면 자체가 나오면 안되는거다. [2:54 PM] 아..근데 버튼은 누를수 없게 했다. [2:54 PM] 그리고 2 card play로 넘어간다. [2:59 PM] 10분만 쉬고 다시 디버거로 확인하자.
hoyoul — 11/22/2023 3:13 PM ui 디버깅이 짜증나는게, ui를 만드는 부분인 build함수가 호출된후 widget tree의 leaf부터해서 올라오는식으로 호출된다. 여기에서 bp를 걸어봤자. 버튼이 눌러져서 수행되는 부분에 걸리는게 아니라, 만들어지는게 걸린다. 그런데 해당페이지가 그냥 다른곳에서 처리되서 넘어가기 때문에 처리하는 부분을 먼저 알아야 한다. 그리고 step out으로 빠르게 원하는 지점으로 이동해야 할듯하다. [3:18 PM] 처음 시작하면 preference의 login데이터를 올린다. loadData()인데, 이것때문에 google 버튼을 누르면 join하는부분이 bp가 안걸리는거다. 코드를 바꿔서 테스트할 수도 있고, shared preference데이터를 시작하자마자 지우게 할 수도 있을꺼 같다.
hoyoul — 11/22/2023 3:21 PM loadData를 주석처리하면 sharedpreference에서 데이터를 못가져오니, login screen을 보여주지 않을까? 그리고 google 버튼을 눌렀을때 어떻게 진행되는지 bp걸어서 확인..그러면 되지 않을까? [3:22 PM] 맞다. 걸렸다.

Figure 57: getx4
break 걸렸는데, 이건 그냥 사족이지만, 버튼을 클릭할때마다 물결모양으로 에니메이션이 보이는데, 이건 material3 design인지 자연님이 구현한건지도 궁금하다. 코드만 봤을때는 별다른 처리가 없긴 한데….여튼 그렇다. [3:26 PM] GoogleSignInAccount? account = await _googleSignIn.signIn(); [3:26 PM] 이 함수가 접속하는 함수다. 이게 수행되면, 다음과 같은 화면이 보인다.

Figure 58: getx5
그럼 나도 이렇게 이 함수만 사용하면 똑같은 화면을 볼수 있을까? 물론 초기화하는 부분이 있겠지만, 당장 이화면을 내 코드에서 보고싶다는 생각이 들었다. [3:33 PM] googleSignedIn package도 깔려 있겠다. google button도 있으니까…그냥 onTap에서 저거 호출해보자. import하는거 까먹지말고…
hoyoul — 11/22/2023 3:43 PM _googleSignIn는 주어진게 아니라 자연님이 만든듯 하다. 따라서 그대로 사용할수는 없고, 저 부분을 까봐야한다. [3:45 PM] final GoogleSignIn _googleSignIn = GoogleSignIn( scopes: [’email’], ); [3:45 PM] _googleSignIn은 scope를 정의한 GoogleSignIn 객체다. 따라서 이부분을 추가해서 test해야 한다.
hoyoul — 11/22/2023 3:53 PM 에러가 난다. [3:54 PM] [VERBOSE-2:dart_vm_initializer.cc(41)] Unhandled Exception: PlatformException(google_sign_in, No active configuration. Make sure GIDClientID is set in Info.plist., NSInvalidArgumentException, null) #0 GoogleSignInApi.signIn (package:google_sign_in_ios/src/messages.g.dart:237:7) <asynchronous suspension> #1 GoogleSignIn._callMethod (package:google_sign_in/google_sign_in.dart:278:30) <asynchronous suspension> #2 GoogleSignIn.signIn.isCanceled (package:google_sign_in/google_sign_in.dart:431:5) <asynchronous suspension> Lost connection to device. [3:56 PM] info.plist에 GIDClientID가 있어야 한다고 한다. 그렇다. 이게 GCP에서 인증 페이지 만드는 작업을 할때, 설정을 다 끝내면 client.plist를 다운받께 했었다. 이게 있으니까 app이 구글에 접근할 주소와 인증정보를 얻었던거다. [3:58 PM] 이건 자연님 branch로 가서 아니 지금 디버깅하는 repo에서 가져와서 설치해보자. [3:59 PM] ios/runner폴더안에 있다. GoogleServiceInfo.plist가 있다. 이것을 꺼내서 똑같이 넣으면 될까?
hoyoul — 11/22/2023 4:00 PM 근데 인증정보는 있지만, 주소정보는 없다. [4:01 PM] 개개의 id별로 주소가 다를꺼라고 생각했는데, 그냥 GoogleSignedIn package에 일반적인 구글주소만 있나보다.

Figure 59: getx6
아…똑같은 에러가난다. googleServiceInfo.plist만 넣어서는 안되고, 어떤처리를 info.plist에서 하던지 direct로 하드코딩하던지 해야한다. 그런 기억이 있다. 예전에 했던 기억이 있다. [4:07 PM] 10분간 휴식하자.
hoyoul — 11/22/2023 4:19 PM 에러 메시지를 보면, ios는 info.plist만 참조한다. 따라서 googleServiceInfo.plist의 내용을 info.plist에 집어넣으면 별도의 googleServiceInfo.plist가 없어도 될듯하다.
hoyoul — 11/22/2023 4:54 PM 아…여기서 GoogleServiceInfo에 대해 알아야 하는게 2가지가 있다. 첫째, GoogleServiceInfo.plist에는 ClientID가 있다. 이것은 인증 정보를 나타낸다. 두번째, ReversedClientID는 접속관련 정보다. [4:55 PM] 접속정보는 URLScheme으로 표현되는데, 이것을 읽을려면 scheme에 맞춰서 읽어야 한다. [4:55 PM] 그래서 URL scheme정보가 info.plist에 있어야 한다. [4:56 PM] 위 두가지 정보를 처리할 수 있게 처리해야 하는데, 이 부분이 제대로 안된다. 자연님의 info.plist를 살펴보자. [4:59 PM] url scheme을 다음과 같이 처리했다.

Figure 60: getx7
그런데 GoogleServiceInfo.plist를 info.plist에 병합하진 않은거 같다. 그렇다면, 하드코딩했다는거 같은데… [5:00 PM] 우선 두번째 URL scheme처리는 알수 있다. [5:00 PM] 밥먹고 와서 또하자.
hoyoul — 11/22/2023 5:02 PM 아니면 URL scheme만 처리하고 GoogleServiceInfo는 동일 폴더에만 있으면 별도의 처리를 하지 않아도 되는지도 모른다. 우선 url scheme만 해보자.

Figure 61: getx8
URL schemes를 설정해줘야 한다.

Figure 62: getx9

Figure 63: getx10
계정 선택후 다시 돌아온다. 계정선택후, 어떤일이 벌어지고 있는지 체크할 필요가 있다. 내생각은 gcp에서 인증이 된이후 토큰같은걸 return받던지 했고, 그런다음 rails와 통신을 할꺼라고 생각한다. 방정보를 얻어온 후에 2 cards play가 있는 page를 보여줄 것이다. [7:43 PM] 이것은 debugging을 좀해서 정보를 알 필요가 있다.
hoyoul — 11/22/2023 7:47 PM 그전에 기록할께 있다. 지금까지 과정을 좀 요약하자. [7:48 PM] google oauth sign in 과정 in flutter [7:52 PM] 예전에 정리했던 글이 있는데, 그것도 참조 해야겠지만, overview를 하면 다음과 같다. app이나 web program을 작성하면 login이 필요하다. 허가받은 사용자만의 app이나 web service를 받게 하기 위해서 인증이 필요한데, 요즘은 oauth라는 방식을 사용한다.
hoyoul — 11/22/2023 7:55 PM open authorizing이라고 해서 authentification을 포함한다고 보면 된다. google이나 apple같은 큰 서비스 업체들에서 cloud service의 일환으로 사용자들에 대한 신원확인 서비스랄까? 그런걸 해준다. 동사무소에서 신원을 보증할수 있는 서류를 발급해주는것과 비슷하다. [7:56 PM] 사용자 입장에선 google이나 apple이 제공하는 이 신원서비스를 거치면 app이나 web을 사용하는 것이다. flutter에서 app을 만드는데, 이것을 할려면 절차가 필요하다. [7:57 PM] (1) GCP 처리 [7:59 PM] app개발자는 사용자에게 보여줄 화면을 작성하는데 consent screen이라고 부르는것을 작성해야 한다. 즉 사용자가 google이나 apple로 로긴하면 우리 app이 google로 부터 몇가지 정보를 얻어서 사용할텐데 괜찮냐? 라는 화면을 만들고, 어떤 resource를 사용할 것이고..뭐 그런작업을 해야 하는데, 개발자가 gcp에 접속해서 해야 한다. 이 과정은 이미 작성했다. 여기에서 우리앱에 대한 정보를 입력해서 credentials얻는 작업도 하고, 여러가지 한다. (edited) [8:01 PM] (2) GoogleService-info.plist 등록 (edited)
hoyoul — 11/22/2023 8:06 PM flutter app 개발자는 사용자에게 google이나 apple에 login할 수 있는 버튼같은 것을 만들고, 사용자가 해당 버튼을 누르면 app이 사용자 대신 gcp에 접속해서 login 서비스를 이용한다고 말한다. 이렇게 대신 접속해서 할려면, 1단계에서 얻은 GoogleService-info.plist가 사용된다. [8:09 PM] plist에는 2가지 정보가 사용된다. 하나는 1단계에서 처리한건데…gcp에 app을 등록해서 credentials를 얻는 과정이 있었다.그때 app을 등록했는데, app에 대한 client ID도 발급받았다. 이것은 우리app이 gcp에 접속할 수 있는 id?라고 봐도 된다. 이게 있어야 접속할 수 있다. 또하나는 url로 된 gcp의 주소인데, 그 주소값도 plist에 있다.
hoyoul — 11/22/2023 8:17 PM client id와 주소값이 들어있는 plist를 app에 저장해야 한다. app에는 이미 info.plist라는 파일에 필요한 데이터를 저장하기도 하고 꺼내쓰기도 하는데, 여기에 GoogleService-info.plist를 등록시키는 작업을 해야 한다. [8:17 PM] 내가 한것은 다음과 같다. (edited) [8:21 PM]

Figure 64: getx11
난 GoogleService-info.plist를 info.plist에 합쳐버렸다. 번거롭게 2개의 plist를 app이 가질 필요가 없기 때문이다. [8:23 PM] 어차피 info.plist에서 key로 검색하기 때문에 상관없다. url scheme에 대해서 간단히 설명하면 다음과 같다. [8:23 PM] URL schemes [8:24 PM] internet resource에는 resource를 식별하기위해서 address를 사용한다. 공식적인 문서나 restful에서 uri를 설명할때 이렇게 설명한다.
hoyoul — 11/22/2023 8:25 PM uri는 uniform resource identifier일꺼다. 아마도…여튼 우리가 쓰는 http 주소도 uri의 일종이다. ftp주소도 uri의 일종이다. slack이나 discord는 irc계열이라서 irc만의 주소형식이 있는데 이것도 uri다. email도 자신만의 주소형식이 있는데 이것도 uri다. [8:27 PM] 예를들어서 http://a.com/b 은 b라는 resource가 어떤 host에 있고, 어떤 protocol을 사용하는지 uri를 http방식으로 만든것이다. hoyoul@whitebrew.com 도 uri를 email protocol방식으로 만든것이다. 둘다 uri를 가르키지만, 형태가 다르다. 이걸 scheme이라고 부른다. scheme이 다르기 때문에 해석할려면 scheme에 대한 정보가 있어야 한다. [8:30 PM] 위에서 google의 주소를 Reversed_client_id로 표시했는데, 이게 보면 알다시피 골때리게 생겼다. 여튼 이것도 scheme인데, 이것을 parsing해서 알아야 한다. 그래서 URL types와 scheme을 plist에 명시해서 app이 이런 uri사용한다. 즉 그 속성은 uri에 일종이고, ana에서 등록된 scheme이니까, 그것대로 해석해서 url뽑아내면 돼..이거다. [8:30 PM] (3) GoogleSignedIn package 사용. [8:31 PM] (1),(2) 단계를 거쳤다면, 코딩하면 된다.
hoyoul — 11/22/2023 8:33 PM 그런데 flutter에서 접속과 관련한 처리도 다 해주는 package를 제공한다. 이 package의 api를 사용하면, 접속할때 필요한 client ID나, reversed client id같은 plist에 있는 정보를 뽑아내고 연결까지 자동으로 해준다. 그게 GoogleSignedIn package에 있는 GoogleSignedIn이란 객체의 signIn()에서 해준다. [8:33 PM] onTap: () async { final GoogleSignIn googleSignIn = GoogleSignIn( scopes: [’email’], ); GoogleSignInAccount? account = await googleSignIn.signIn(); if (account != null) { print(account.email); } [8:36 PM] 이렇게 해주면 app은 gcp에 접속해서 사용자 login부탁하러 왔습니다.가 된다. gcp에선 어! 그러면 저번에 작성하신 사용자동의서 드릴테니 고객한테 사인받고 인증완료되면 원하시는 scope 드릴께요가 된다. 그 과정을 ui에서는 다음과 같이 나타난다.

Figure 65: getx12
사용자에게 동의하는지…물어본다.

Figure 66: getx13
사용자가 인증 성공한다면? [8:37 PM] 원했던 scope인 email을 얻을수 있을 것이다. [8:37 PM]
[8:38 PM] 여기까지 대충 정리는 끝냈고, scope인 email을 받아왔을까? 확인해보자. 디버깅하는게 편하다. print문을 쓰는것보다 디버거를 돌리자.

Figure 67: getx14
근데 이미 print문이 있기 때문에, 화면에 출력한다. [8:45 PM] 제대로 email을 가져오는것을 확인했다. [8:46 PM] 그러면 rails에 접속해서 무언가를 통신할건데…이건 내일하자. [8:47 PM] 우선 동작만 확인했는데…여기엔 문제가 있다. model, controller를 생각한게 아니다. 그냥 동작만 확인한것이다. 이런 business logic을 따로 빼내야 한다. November 23, 2023
11.23
email을 받은 다음, 무엇을 하나? [8:35 AM] 그리고 왜 email을 요청했나? [8:36 AM] 모른다. 디버깅을 해서 추론하는 수 밖에 없다. 왜냐 어떤 설명이나 문서가 없기 때문이다.
hoyoul — 11/23/2023 8:53 AM GoogleSignedIn Package에서 제공하는 중요한 class인 GoogleSignInAccount를 좀 살펴보자. class GoogleSignInAccount implements GoogleIdentity { GoogleSignInAccount._(this._googleSignIn, GoogleSignInUserData data)
displayName = data.displayName,
email = data.email,
id = data.id,
photoUrl = data.photoUrl,
serverAuthCode = data.serverAuthCode,
\_idToken = data.idToken;
[8:55 AM] 그리고 GoogleSignedIn이란 class를 보자. GoogleSignIn({ this.signInOption = SignInOption.standard, this.scopes = const <String>[], this.hostedDomain, this.clientId, this.serverClientId, this.forceCodeForRefreshToken = false, }) { [8:56 AM] GoogleSignedIn class (edited)
hoyoul — 11/23/2023 9:13 AM googleSignedIn class는 GoogleServiceInfo.plist에서 얻은 clientID와 reversedClientId URI를 parsing해서 hostdomain, serverClientID와 같은 정보로 객체를 만든다. 이 class의 signIn 메소드를 호출하면 oauth 서비스를 사용하고 싶습니다. 라고 gcp에 scope와 함께 요청하게 된다. 이것은 위에서 말했던 그 과정, gcp가 그럼 ‘사용자 동의서 화면을 제공하고 인증과정을 하겠습니다.’ 를 의미한다. 이 인증이 성공하면 google에서 제공하는 객체가 GoogleSigInAccount라는 객체다. 이 객체에는 요청한 email외에도 displayName, id, photoUrl, serverAuthCode, _idToken이 있다.이걸 확인해봐야 한다.
hoyoul — 11/23/2023 9:31 AM debugger를 할때 thread가 exit한다. 뭔 문제가 있다는거다.
hoyoul — 11/23/2023 10:01 AM 슈퍼좀 갔다오자.
hoyoul — 11/23/2023 10:30 AM

Figure 68: getx15
debugging했는데, displayName을 받아온다. email주소 뿐 아니라, photo까지도 받아올수 있다. photo를 받아오면 avatar도 구현할 수 있게 된다. photo url로 접속해보자. => profile 이미지 가져온다. 확인. (edited)
hoyoul — 11/23/2023 10:38 AM 그다음 과정을 진행하자.
hoyoul — 11/23/2023 11:02 AM var auth = await account.authentication; bool result = await KholdemAuthApi() .login(auth.accessToken, auth.idToken, account.serverAuthCode);
=>이게 rails하고 통신하는 부분일거 같다. [11:06 AM] 3개의 인자를 주는데, auth는 authentication항목이다. GoogleSignedInAccount객체의 member변수로 되어 있다. 근데 _idToken은 authentication에 포함 안되어있는데..한번 까봐야겠다. [11:08 AM] 그리고 ServerAuthCode는 null로 받는데, 이게 시료와 simulator의 차이일수도 있다. [11:08 AM] 이건 시료에서도 확인해봐야 한다.
hoyoul — 11/23/2023 11:37 AM 여기서, 좀 이해가 안가는게 있다. flutter app에서 인증을 받았다. 그래서 token을 받았는데, 이것을 다시 서버로 보낸다. 왜 보낼까? 쓸 일이 있어서다. 어떤 작업을 하길래 token이 필요한걸까? 음…그리고 flutter가 토큰을 받은 이유는 사용자를 대신해서 google에서 resource에 대한 접근을 할수 있게 하기 위함인데…이것을 server에서 대신하라 이건가? 좀 살펴봐야겠다. [11:40 AM] 중복의 가능성은 없는가?
hoyoul — 11/23/2023 11:49 AM => 순상님하고 통화를 했다. 정리를 하면 다음과 같다. user가 google인증을 완료하면, flutter app에서 3개의 token을 받는다. 이 token의 역할은 user를 대신해서 google에 접근할수 있는 권한이 있는 access token, 그리고 user에 해당하는 id token이라서, 즉 한마디로 user대신 google에 접속해서 정보를 가져올수 있는 권한이 있는 토큰들인데, 이것을 rails로 다시보낸다. 여기까지의 과정은 맞다고, 순상님이 더블첵해줬다. (edited) [11:50 AM] 그런데 왜 그런 toekn을 rails에게 보내냐? rails가 user대신에 google server에 접근해서 뭐 하는게 있냐? 라는거에 대해서 [11:52 AM] 순상님은, rails가 하는 것은 flutter에서 받은 토큰이 제대로 인증을 거쳐서 확인됐는지 rails가 다시 한번 체크를 한다고 한다. 아무래도 server client통신을 하는데 위변조된 토큰으로 인증받은 flutter가 sever와 통신할 수도 있는거니까…flutter에서 맞다고 하면 맞는게 아니다. 네트웍에선 외부에서 어떤것도 연결을 하고 사용하려면 인증을 거쳐야한다. flutter에서 받아온 token이 맞는지 안맞는지는 rails에서도 확인해야한다. 그렇게 확인하기 위해선 token을 받아서 다시 google에 문의하면 된다고 한다. 즉 rails에서 하는것은 제대로 토큰을 받아왔는지 google에 확인한다고 한다. (edited) [11:53 AM] 그리고 그것을 사용하는데 있어서 확장스펙이란게 존재하는데, 확장스펙을 사용하진 않고, 즉 구글의 사용자 정보를 가져온다던지…직접 연결해서 resource를 처리하는건 없다는거 같다. 다만 token의 validation만 확인한다. 라고 들었다. (edited) [11:56 AM] 다시 이어서 하면, 3개의 token을 받아서 보내는 부분을 보고 내 코드에서 동작하는지 확인해야 한다.
hoyoul — 11/23/2023 12:14 PM Future<bool> login( String? accessToken, String? idToken, String? authCode) async { try { var response = await kholdemAuthApi.post(’/google_oauth2_with_id_token’, data: jsonEncode({ “omniauth.auth”: { “credentials”: {“token”: accessToken}, “extra”: {“id_token”: idToken}, “server_auth_code”: authCode } }));
var userToken = UserToken(serializer.deserialize(jsonEncode(response.data)));
SettingController.to .setUserToken(userToken.authToken, userToken.authEmail);
return true; } on Exception catch (e) { if (e is DioException) { print(e.response); } else { e.printError(); }
return false;
}
} [12:17 PM] 이게 rails와 통신하는건데, 3개의 token을 보낸다. acess token, id token은 이미 말했듯이 user를 대신하는 권한을 가진 token이고 serverauth token은 app이 처음 google에 로그인좀 부탁하러 왔습니다.라고 할때 사용되는 토큰이다. 여튼 3개의 토큰을 json으로 전달한다. [12:19 PM] 받은걸 deserialize로 객체화하는데, UserToken객체인데…이부분이 이상하다. [12:21 PM] 왜냐면, 이미 email같은건 flutter app이 가지고 있고, rails로 보낸건 token validatation을 확인하기위해서 token을 보냈기 때문에 rails에서 token이 valid하네만 확인할 수 도 있고, 확인목적으로 profile도 가져온다고 했으니, 내부적으로는 user객체를 만들었을수도 있다.
hoyoul — 11/23/2023 12:24 PM 그러면 어떤걸 return받는지 확인해봐야 하는데, 코드만 봐선, 이미 보낸거 다시 받는다는 느낌이다. 이미 알고 있는 email,하고 token들을 왜 UserToken에 저장하지? 물론, rails서버에서 하는 validation이 끝나야 진짜 이 user가 맞는지 최종확인이 되었기 때문에, 단지 flutter app에서 받은 email이나 token들을 그상황에선 저장할수 없고, rails서버의 확인이 끝난후,, 그때 최종확인이 되니까, UserToken이란 객체를 만들고 정보를 저장한다고 볼수도 있다. (edited) [12:26 PM] 물론 내 뇌피셜이다. [12:26 PM] 우선 디버거로 확인하고, 그다음 내 코드에 적용시켜보자. [12:26 PM] 밥먹고 와서 보자.
hoyoul — 11/23/2023 1:32 PM rails 서버 통신 [1:33 PM] 오전에 google login을 확인했고 코드도 동작여부만 확인했다. [1:33 PM] rails와의 통신은 json으로 주고 받는다. dio란 package를 사용한다. [1:34 PM] 주소는 다음과 같다. dio.options.baseUrl = “https://kholdem.fly.dev/auth ”; dio.options.contentType = “application/json”; [1:34 PM] var response = await kholdemAuthApi.post(’/google_oauth2_with_id_token’, data: jsonEncode({ “omniauth.auth”: { “credentials”: {“token”: accessToken}, “extra”: {“id_token”: idToken}, “server_auth_code”: authCode } [1:35 PM] /google_oauth2_withi_id_token으로 보내면 rails에서 처리한다.
hoyoul — 11/23/2023 1:48 PM

Figure 69: getx17
flutter에서 보낸 토큰중에 idToken이란게 있다. 이것은 우리 앱을 사용한 user를 나타내는 id로 보면 되는데, 이것을 rails로 넘겼더니, 다시 auth_token이란 이름으로 return했다. 그리고 email도 넘겨줬다. [1:53 PM] 동작시키는거야 문제가 없다. 그냥 http로 보내면 받는거고, 보내는 코드도 간단하니까.
hoyoul — 11/23/2023 1:57 PM 좀 정리가 필요하다. (edited) [2:03 PM] 왜냐면, user가 login버튼을 누르면 진행되는 process가 두가지 가 있다. [2:03 PM] 두 case인데, [2:03 PM] 첫번째는 처음 login하는 경우다. [2:03 PM] 이건 회원가입과 같다.
hoyoul — 11/23/2023 2:04 PM 즉 구글의 인증을 받고, 정상적인 사용자임이 확인 되었다면, rails서버나 flutter에서 회원가입을 한것처럼 동작해야 한다. [2:05 PM] user name, email, pw에 해당하는 token이 user레코드로 만들어진다. [2:05 PM] 두번째 case가 있다. [2:05 PM] 이미 회원가입이 끝난경우다. [2:06 PM] 그러면 user가 login버튼을 누르면, google인증을 해서 다시 회원가입할 필요가 없다. [2:06 PM] 그래서 flutter에서 shared preference에서 checking을 해서 이사람이 처음 login하는지, 이미 회원 db에 있는 사람인지를 check한다. [2:07 PM] 그런데, 지금 첫번째 case에 해당하는 회원가입 처리 process를 보면, [2:08 PM] token을 rails로 보내면서 응답으로 받은 useremail과 idtoken만을 받는다. 이 말은 사용자 정보에 해당하는 username을 서버와 클라이언트 모두 처리하지 않는다는 얘기다. [2:08 PM] 물론 server는 처리할 수도 있다. 근데, 처리를 한다면 return값으로 username도 return해주는게 맞다. [2:10 PM] 왜냐? idtoken과 useremail은 이미 flutter가 알고 있음에도 server에서 return된 id_token,user email을 등록해서 사용하는것은 server가 최종적으로 vericated해서 확정했기 때문에 return값을 사용하는 것이다. username도 똑같다. flutter에서 useremail처럼 이미 알고 있는 값이지만, 서버의 verification을 기다린것처럼 username도 return되길 기다리는데 안온것이다. 만일 flutter가 username을 안다고 해도 독자적으로 처리해서는 안된다. (edited) [2:11 PM] 여튼, 게임에서 username을 보여줄려면 useremail을 보내준것처럼 똑같이 username도 rails서버가 보내줘야 한다. 즉 rails server가 회원가입처리를 했고 지금 login된 회원의 정보를 다시 client에게 return해줘야 하는것과 같다. 결론적으로, rails server는 username, useremail, 그리고 pw에 해당하는 id_token으로 줘야 한다는 얘기다. flutter에서 username을 보낼수도 있지만, server에서 어차피 토큰으로 구글 서버에서 email만 받아오는게 아니라 display name도 받아왔어야 하는데, 그부분이 안되어 있다는 것이다. 이건 google에서 받아오는것을 debugger로 까보지 않고서는 모르는 내용이긴 하다. 여튼 수정이 필요하다. 왜냐면 지금은 email을 사용자 이름으로 생각하겠다는건데, google에서는 username과 email은 필수로 등록하게 되어 있다. 게임을 하는데, email을 user이름으로 표시해서 게임을 진행할 생각은 안했을것이다. 여기서 어떻게 username을 만들어서 할것인지 생각했을텐데, 그럴필요가 없다. 그냥 displayname을 username으로 사용하면 된다. (edited)
hoyoul — 11/23/2023 2:11 PM google에서 가져오는 정보중에 displayName이 있다는것을 모른것이다. (edited) [2:17 PM] 여튼 그렇다.동작이 되는지 테스트 해보자. [2:18 PM] 10분만 쉬자.
hoyoul — 11/23/2023 2:34 PM 나는 user에 해당하는 model이 있어야 한다. [2:37 PM] user의 member변수로는 username, id_token만 유지해도 되고 email이 있어도 된다. email이 있다면, 이것은 user profile이라는 버튼에서 보여줄 내용에 포함되면 된다. 테스트를 끝내고 server에서 username,email,idtoken을 보내달라고 해도 되고, local에서 바꿔서 테스트해도 된다. [2:37 PM] 우선 설계를 해야 하는데, user라는 model을 만들고, controller를 만들어야 한다. [2:39 PM] 그런데 display name은 말씀처럼 표시하는 이름으로 쓰려고 한게 맞고, 서버 user model에도 name 필드를 만들어뒀는데, 서버에 사용자 생성할 때 빼먹은거 같네요;; 나중에 보완해야겠어요. email은 id 대용이라고 생각하시면 됩니다. 사용자 키에요. (edited) [2:39 PM] 이런 내용이 있었네.
hoyoul — 11/23/2023 2:42 PM 이건 return할때 name을 같이 보내달라고 해야 한다. 그래야 name을 게임에서 보여줄 수 있기 때문이다. 이런처리가 flutter에는 안되어 있다. [2:42 PM] email은 rails에서 key로도 사용할수 있는데, [2:44 PM] key로 사용하는경우 별로 없기때문에, 왜냐면 rails에서는 primary key는 자동으로 만들어주기때문에 직접 email field를 primary key로 지정하진 않았을것이다. 다만 find할때…사용하는 모양이다. [2:46 PM] 지금상태에선 사용자이름에 해당하는 User모델의 멤버변수가 있긴해야 한다. email로 게임판에 보여주는건 아니니까.. [2:47 PM] null로 하고 나중에 보내주거나, 아니면 내가 rails 소스를 수정해서 처리해도 된다.
hoyoul — 11/23/2023 2:54 PM 지금 작업중인걸 어떻게든 처리해서 commit으로 올린후에 user model을 만들어서 올려야 할듯하다. [2:55 PM] 미완성이라고 올리더라도, 동작을 하는데 문제가 없어야 하고, 에러나 warning이 없게 올려야 한다. [2:55 PM] 이작업을 먼저하자.
hoyoul — 11/23/2023 3:03 PM 좀 일이 많아진다. 여러파일을 만들어놨다. 없앨건 없애고 정리하자. [3:04 PM] 테스트를 너무 많이 했다. test폴더를 만들고 했었어야 했다.
hoyoul — 11/23/2023 3:56 PM commit을 올리긴 했다. [3:57 PM] 이제 model관련 작업을 하자.
hoyoul — 11/23/2023 4:22 PM 그냥 무턱대고 User를 만드는것보다 좀 생각을 하자. [4:24 PM] 섯다게임에 사용되는 객체는 뭐가 있을까? User, GameRoomlist, GameRoom, Game class가 사용될꺼 같다. 왜냐면 roomlist를 서버로 부터 얻어오고 뭐그런걸 봤다.
hoyoul — 11/23/2023 4:31 PM 음..좀 복잡하다. [4:33 PM] User는 기본적으로 name,email,pw(id_token), pot 정도가 언뜻 떠오른다. 여러 member 변수가 추가될 수 있지만, 이렇게 만들고, setter,getter만들고… (edited)
hoyoul — 11/23/2023 4:40 PM 우선 나는 getX를 사용하지만, getX에서 controller와 model을 섞어서 사용하는데, 이것을 분리해서 작성할려고 한다. 즉 swing모델이 아니라 smalltalk 모델인데, 즉, mvc로 작성할것이다. [4:42 PM] class UserModel { String userName; String userEmail; String password or idToken; int? pot;
User({required this.name, required this.email, required this.password, this.pot=null}); }
이렇게 간단히 model을 만들고… [4:43 PM] controller에서 user에 obs를 걸고, [4:43 PM] obs걸 필요가 있는지 모르겠다. [4:44 PM] 이 데이터가 변경이 되면 ui도 변경이 되는가?
hoyoul — 11/23/2023 4:50 PM 변경되지 않는다고 본다. user객체를 만들고 이것을 access하는 것은 두가지 경우가 있다. 하나는 회원가입, 즉 새로운 사용자라서 rails에 token을 전달해서 rails에서 record를 만들고, ok message를 보낼때, 근데 보니까, server에서 status메시지도 안보낸다. useremail과 id_token만 보낸다. 만일 validate한 결과 문제가 있으면 그냥 null값을 보낸다는 건가? [4:52 PM] 그리고 두번째, 이미 있는 사용자라서 preference에서 가져올때 설정한다. 이렇게 설정된 user객체를 만드는 것까지만 해보자.
hoyoul — 11/23/2023 4:58 PM username의 경우는 view에서 보인다. 그래서 obs를 걸수는 있다. 그런데 그것이 변경될 수가 있는가? [5:05 PM] 생각나는대로 쓰자.
hoyoul — 11/23/2023 5:05 PM login버튼을 누르면, view에서 작업한걸, getXcontroller의 A()를 호출하는 식으로 변경해야 한다. 이 controller의 A()가 하는일은 [5:05 PM] user preference에서 확인 [5:07 PM] yes -> game page로 이동, 그전에 rails하고 어떤 통신이 있을수도 있다. 즉 login된 사용자정보를 rails server에 보내준다. page를 이동할때는 getx의 routing을 사용한다. [5:08 PM] no -> userpreference에 없다는건 새로운 사용자다. google인증을 해야 한다. signIn()를 호출하고 그 정보를 rails server에 전달해서 email,토큰을 받아와서 user객체 세팅하고, game page로 이동. [5:08 PM] A()의 naming은 밥먹고 와서 구현도 해보자
hoyoul — 11/23/2023 6:14 PM 2 과정은 그냥 login과 같기 때문에 flutter가 email하고 토큰을 전달하면, server에서는 email키에 해당하는 token을 redis에서 가져와서 check하고 ok를 던져줘도 되고, ok라면 email을 key로 하는 game room정보를 redis에서 가져와서 던져주면 된다. [6:15 PM] 3은 회원가입이라서 서버는 db에 저장을 해야 한다. [6:15 PM] 가입후 ok와, user관련 정보를 전달해 줘야 한다. [6:16 PM] 이것을 diagram으로 표시하면 어떨까?
hoyoul — 11/23/2023 6:23 PM login이나 회원가입이나 성공하면 그냥 room list를 줘도 상관은 없는데…여튼… [6:24 PM] emacs에서 plantuml을 사용한지도 오래되서 다 까먹었다.
hoyoul — 11/23/2023 7:29 PM import ‘package:get/get.dart’; //import ‘package:kholdem_v1/model/user_model.dart’;
class UserController extends GetxController { // google에 연결해서 token 받는 function usin oauth Future<void> fetchTokenFromGoogle() async {}
//rails에 token 전달하는 function Future<void> sendTokenToRailsServer() async {}
//shared preference에서 정보 가져오는 부분 Future<void> fetchUserInfoFromSharedPreferences() async {
} } [7:29 PM] 간단하게 business logic을 처리하는 controller를 생각해봤다. [7:31 PM] login버튼을 누르면 해야할일을 절대로 view에서 하면 안된다. 그렇게 되면 스파게티 코드가 되버린다. getX를 사용하기 때문에 GetXcontroller에서 business logic을 처리한다. business logic을 view와 구분만 하면 된다. 여기서 어떤게 구조화하는지 어떤걸 모으는지 이런건 다 부차적인거다. 하나만 하면된다. view 와 logic만 분리하면 되는거다.
hoyoul — 11/23/2023 7:58 PM 고민이 되는게, page라는 논리적 단위에 맞춰서 model view controller를 분리하는게 맞아보이긴 하다.그런데 getX는 page라는 단위로 국한되어 설계 되어 있지 않다. [7:58 PM] 어떻게 할것인가? 그냥…user model로 만든것도 page를 생각하고 만든건 아니긴 하다.
11.24
login하면서 좀 이상한게 있다. google의 oauth를 사용해서 login하면 3개의 token을 받는다. id_token, accessToken, serverAuth토큰이다. [8:47 AM] id_token은 사용자 정보를 나타내는 token이고, accessToken은 접근권한을 나타내는 token이다. accessToken은 만료일이 있다. 즉 refreshToken을 받는건 accessToken을 재발급받는것이다. [8:47 AM] 그런데 우리앱에선 id_token을 rails에 주고 rails에서 id_token을 돌려준다.
hoyoul — 11/24/2023 8:53 AM 그리고 자연님 코드에서 bearer를 사용해서 넘겨주는 토큰도 id_token아닐까 한다. 코드를 보면 복잡하다. 왜냐면 userToken이란 이름과 authToken이라는 변수를 같이 사용하기 때문이다. 이게 햇갈리는게, userToken이 IdToken을 의미하는 것임과 동시에 이게 인증토큰이라서 authToken이라고도 이름 지은거 같다. 근데, 만료일과 관련된건 accessToken인데…좀 이상하다. 이게 디버깅을 확인할 필요가 있다. 코드로는 좀 어렵다. [8:55 AM] 그리고 로긴과정에서 생각할께, app이 백그라운드로 빠지는 경우가 있고, logout하는 경우가 있다. app이 백그라운에 있다가 다시 active될때와 logout할때는 처리방식이 다르다. 이것도 생각해야 한다.
hoyoul — 11/24/2023 9:05 AM 우선 내가 하고 있는거 완료한후 순상님한테 물어보고 수정할꺼 있음 수정하자.
hoyoul — 11/24/2023 9:18 AM 자연님의 코드에 보면, bool isValidToken() { // 토큰 유효성 검사 -> TODO : 유효기간 체크 return (userToken.isNotEmpty ?? false) && (userEmail.isNotEmpty ?? false); } [9:19 AM] flutter app에서 유효기간 체크를 한다. todo로 했놨는데, 뭐 이거야 access token까면 만료일이 있으니까 거기에 timer걸어서 다시 rails로 요청해서 acessToken다시 받아와서 갱신하면 되는건데… [9:22 AM] 서버에서 오는 데이터중에 exp라고 있다. 이게 expired date를 나타내는거 같은데 [9:23 AM] 별도의 값으로 넘어온다. rails에서 access code에서 만료일을 계산해서 별도로 넣어줬는지는 모르겠다.
hoyoul — 11/24/2023 9:26 AM 지금짜는건 controller에 login여부를 나타내는 상태데이터를 만들었다. 그리고 이것을 처리하는데 sharedPreference에 저장된 값을 이용하는건데…shared preference와 로긴 관련해서 좀 생각하고 있다. 단순히 id,token을 집어넣고 있는지 확인하는게 아니라….좀 생각해야한다. [9:33 AM] Shared Preference 적용 (edited)
hoyoul — 11/24/2023 9:34 AM
- flutter pub add shared_preferences (edited)
[9:35 AM] flutter upgrade가 있어서 3.13에서 3.19로 upgrade했다. [9:36 AM] flutter가 버전을 타는지는 모르겠다. rails는 예전엔 버전을 많이 탔는데…우선 flutter를 믿고 upgrade했고 shared_preferences를 설치했다. [9:37 AM]
- login이 성공했을때 preference에 저장한다.
[9:39 AM] login이 성공했다는건, login버튼을 눌렀을때 google로 접속해서 token을 얻어오고 rails 서버와 통신해서 ok싸인을 얻으면 성공한것이다. [9:40 AM] 실패했을때는 try catch로 다 잡아주고 error를 print만 하자. popup 메뉴는 이런건 나중에 생각하고 console로 뿌리자.
hoyoul — 11/24/2023 9:42 AM login버튼 누르면 GexController의 함수 호출 처리가 가능한지 check (edited) [9:44 AM] view의 login버튼에선 Get.find함수를 사용해서 controller를 가져와서 함수 호출을 하면 된다.
hoyoul — 11/24/2023 9:49 AM [수정요] 내가 착각한 게 있다. 몇몇 버튼들을 container로 제작했다. 왜냐면, 어차피 gestureAdapter를 붙이면 tap을 할수 있고 모양도 비슷하니 통일하자고 한건데, button과 container에는 ui가 차이가 있다. button에는 눌렀을때 물결표시처럼 animation이 나타나지만 container에는 그런게 없다. 우선 이렇게 기록만 남겨두고 나중에 수정해야 한다. (edited)
hoyoul — 11/24/2023 9:59 AM (1) getXController를 상속하는 class를 만들고, 이를 등록 [10:00 AM] class를 만들었다. 등록은 entry point에서 자연님이 했는데, 이건 간단한 예제를 표시할때 하는것이고, entry point에서 하면 비동기처리때문에 세팅도 해주고…여튼 나는 그냥 app이 시작되면 widget tree의 root에서 해주면 된다고 생각한다. [10:02 AM] 왜냐면 getX가 app뒤에 떠 있어서 전반적인걸 controll하기 때문에 app widget이 시작될때, Get.put(UserController())
hoyoul — 11/24/2023 10:18 AM 이렇게 해주면 어떤 widget에서도 controller를 사용할 수 있기 때문이다. [10:22 AM] 아..근데, 보통 main에서 해주는게 일상적이라고 한다. 자연님이 한게 맞다. main()에서 해주자. [10:22 AM] 근데 이유는 모르겠다. [10:23 AM] 어쨋든 해주면 되는데, 이게 확실하게 위치가 정해져 있지 않다면 관례를 따라야 가독성이 좋아지기 때문에 사람들이 하는 방식대로 하는게 맞다. [10:25 AM] 그리고 button을 누르는곳에서 onTap: () async { UserController controller = Get.find<UserController>(); controller.test(); }, [10:25 AM] test함수 호출되는지 확인하자.
hoyoul — 11/24/2023 10:26 AM

Figure 70: getxcontroller1
확인했다. [10:27 AM] controll과 view가 분리된것이다. [10:28 AM] (2) login버튼 누르면 google oauth 접속해서 token 받아오기

Figure 71: getxcontroller2
controller에서 처리한다. [10:34 AM] 제대로 가져왔다. [10:36 AM] 이건 login이 성공한게 아니다. login은 rails 서버에 접속해서 응답을 받아야 성공한것이다. 그때 shared preference에 저장을 하는것이다. 물론 이런 과정을 수행할때 protocol을 rails server에 물어봐서 처리할께 있다. 하지만, 우선 구현부터 하자. [10:36 AM] (3) 받은 token을 rails에게 보내기 (edited)
hoyoul — 11/24/2023 10:41 AM 간단히 테스트로 userController내에서 할수도 있지만, rails와의 통신은 game전반내내 여러부분에서 사용된다. 그것이 dio이던 웹소켓이던…그래서 별도의 rails와 통신을 담당하는 service내지는 class를 만드는게 맞다고 본다. 혼합해서 쓰면 복잡하고 알아보기 힘들다. [10:42 AM] 근데 rest api를 쓰지않고 websocket을 사용하기로 task가 정해졌기 때문에 naming도 그것에 맞춰서 해야 한다.
hoyoul — 11/24/2023 11:23 AM web socket service를 만들자. [11:25 AM] web socket package는 많다. 그냥 구글링하면서 몇개 봤는데, web_socket_channel이란 package가 많이 사용되는듯 하자. 어차피 내부구현은 다 비스무리하기 때문에.. [11:25 AM] https://pub.dev/packages/web_socket_channel
인기가 100%다. 자주 사용되는거 같다.
hoyoul — 11/24/2023 11:30 AM dart pub add web_socket_channel [11:30 AM] 설치를 한다. [11:32 AM] flutter를 update하고 다음과 같은 메시지가 나온다. [11:32 AM] Changed 1 dependency! 12 packages have newer versions incompatible with dependency constraints. Try `dart pub outdated` for more information. [11:33 AM] dart pub outdated를 하니, dart pub outdated
Showing outdated packages. [*] indicates versions that are not the latest available.
Package Name Current Upgradable Resolvable Latest
direct dependencies: all up-to-date.
dev_dependencies: flutter_lints *2.0.3 *2.0.3 3.0.1 3.0.1
1 dependency is constrained to a version that is older than a resolvable version. To update it, edit pubspec.yaml, or run `dart pub upgrade –major-versions`.
hoyoul — 11/24/2023 11:41 AM lint는 flycheck같은거라서 update안해도 상관없다. 그런데 나는 flycheck사용하기 때문에 update할것이다. [11:41 AM] dev_dependencies: flutter_lints: ^3.0.1 [11:41 AM] 이렇게 하면된다. [11:44 AM] 적용은 flutter pub get [11:44 AM] 하면 된다.
hoyoul — 11/24/2023 11:50 AM webSocket을 class로 만든다. class를 객체로 만들어 사용할때는 3개의 method만 사용하면 될듯 싶다. socket연결,데이터전송,socket닫기. 이것만 있으면 될듯한데, 다른게 있어야 하나?
hoyoul — 11/24/2023 12:02 PM 데이터를 받기도 해야하니까, 데이터 수신도 있어야 한다. [12:07 PM] / Copyright (c) 2018, the Dart project authors. Please see the AUTHORS file / for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file.
import ‘package:web_socket_channel/io.dart’; import ‘package:web_socket_channel/status.dart’ as status;
void main() { final channel = IOWebSocketChannel.connect(‘ws://localhost:1234’);
channel.stream.listen((message) { channel.sink.add(‘received!’); channel.sink.close(status.goingAway); }); } [12:08 PM] example은 간단하다. socket연결, sink를 사용하니까 stream사용한다는 얘기고 [12:08 PM] 메시지를 받으면 received로 보내고 닫는것이다. [12:09 PM] 이렇게 직접 사용해도 되고, class로 만들어도 상관없다.
hoyoul — 11/24/2023 12:10 PM 난 class로 만들기로 했다. 왜냐? 자주 사용되니까…어떨때는 닫고 다시 쓰고 뭐 이럴수도 있으니까..보통은 이런것들 socket pool을 만들어서 제어하긴한다. 예전엔 다 그렇게 했다. (edited) [12:11 PM] 직접 사용하면 관리가 안된다. 나중에 싱글톤패턴을 사용해서 socket pool로 만들 수도 있기때문에 class로 만드는게 더 낫다. (edited)
hoyoul — 11/24/2023 12:32 PM socket이야 간단하니까, rails server로 연결해보자. [12:32 PM] rails 연결
hoyoul — 11/24/2023 2:27 PM 나는 보통 null값을 assign을 하기 싫어서 string?를 많이 사용했다. 근데 late도 많이 사용하는듯하다. [2:33 PM] 음…socket으로 보낼때, 그냥 보내는 함수에 data를 넣어보내면 안된다. 현재 json으로 받게 되어있기 때문에 json처리를 해야할듯하다.
hoyoul — 11/24/2023 3:03 PM json api 1.0을 사용한다면, 모든것을 oop관점에서 생각하고 처리해야 한다. [3:08 PM] 음..좀 생각을 해야할듯하다.
hoyoul — 11/24/2023 3:17 PM data, type, id라는 값을 채워서 보내주는데, id는 instance를 의미하는것이고, 전체는 class에 해당한다. rails에선 resources 혹은 resource로 routes에서 처리만 해주면 url만들어주는거지만, flutter에선 oop로 설계를 해야하는데… [3:17 PM] User라는 class가 있으니까, user1에 해당하는 것을 json으로 나타낼 수 있을것이다. [3:18 PM] 각각의 user는 id_token,accessToken, email,name등이 attribute로 있으니까…이것을 jsonApi model을 상속받게해서 사용하면 될거 같기도 한데… [3:19 PM] 물론 User클래스에는 다양한 속성이 있다. 이것이 rails server의 table과 매핑이 되어야 하는것이다. [3:20 PM] user가 가지는 속성은 엄청많을 것이다. 이것을 어떻게 record로 가지고 있는지 확인해보자.

Figure 72: getxcontroller3
name, cards,balance, email을 가지고 있는데, token들은 가지고 있지 않다. [3:26 PM] 설계하기 나름이라서, 안가지고 있어도 상관은 없는데…. [3:27 PM] user에 속성이 이것밖에 없을까? 중요한건 나도 이것을 or mapping시키듯이 class로 만들어야 한다는 것이다. [3:29 PM] 토큰이 필요없나? user별로 token이 있을텐데, 왜냐면 나는 token을 class에 포함시켜야 하는데, token이 포함된 user class는 json으로 직렬화해서 그대로 전달하면, rails에서도 그대로 받아서 그대로 record갱신도 가능한거라서…server와 client가 일치되야 하는게 맞긴하다. [3:30 PM] 이건 좀 생각해보자. 물론 없어도 처리가 가능한데, json1.0을 사용하는 이유가 parsing할때 조금이라도 수동 작업을 안할려고… 이둘을 일치시켜야 하기 때문이다.
hoyoul — 11/24/2023 3:31 PM 수동으로 parsing해서 어떤것은 record에 값으로 포함시키고 어떤것은 안포함시키고 이런거는 dart:convert에서 그냥 json만들고 json받아서 parsing하면된다. [3:32 PM] json1.0은 별거없다. 그냥 parsing 수동으로 안할려고 하는것일뿐…. [3:33 PM] 지금은 우선 위의 내용들 다 User class에 포함시키자. [3:37 PM] name도 일치시켜야 한다. [3:38 PM] rails는 table이 이미 or mapping되서 저게 곧 객체다. 근데 flutter는 or mapping을 만들어주듯이 해야 한다. 그래서 똑같은 member를 갖는 User class를 만들어줘야 한다. 그래야 serialize,deserialize하는게 자동화되고, 이게 json1.0에서 사용하는 방식이다.

Figure 73: getxcontroller5
맞춰서 만들자. [3:43 PM] 여기서 cards가 붠지 모르겠다. [3:44 PM] Token을 위한 table이 별도로 있다.
rails에서 사용하는 omnioauth다.
[3:47 PM]
그런데 firstname하고 lastname은 display name에서 space로 분리해서 저장하는듯 하다.
[3:47 PM]
user_id, uid는 뭘까?
[3:48 PM]
똑같은 형태의 class를 만들어야 한다.
[3:49 PM]
omni_auth_infos라고 복수로 되어 있는데, 이건 rails 컨벤션이다. flutter에선 class로 단수표기를 사용하기 때문에 OmniAuthInfo라는 class를 만들어야 한다.
hoyoul — 11/24/2023 3:51 PM User와 OmniAuthInfo는 has one relationship을 갖는다. [3:51 PM] OmniAuthInfo class 만들기. [3:52 PM] 근데…이건 좀 이상하지 않나? ominAuth에는 token들하고 userid만 있으면 되지 않나? 왜 email이 있지? 이거 뭐지 [3:53 PM] user하고 relationship이 있는데, [3:53 PM] photo도 당연히 user의 속성인데…
hoyoul — 11/24/2023 4:01 PM 음 이건 좀 물어봐야 하는데, 모았다가 좀 물어봐야겠다. login하는데 있어서 이전꺼 하고 [4:02 PM] 지금도 그렇고 몇개는 같이 수정을 하던 해야하는데… [4:02 PM] 우선 동작이 우선이다.ㅋ
hoyoul — 11/24/2023 4:15 PM 근데 이걸 먼저 해결해야지 뭘 만들거 같긴 하다. 내생각에 user하고 omni하고 1:1 mapping처리가 has one으로 처리가 되어 있을꺼라고 예상되는데, 한번 소스를 보자. [4:19 PM] class OmniAuthInfo < ApplicationRecord belongs_to :user end [4:20 PM] 우선 user와 omniAuth는 1:다 관계다. (edited) [4:20 PM] class User < ApplicationRecord include Omniauthable
has_one :game_room_user has_one :game_room, through: :game_room_user has_one :game_player, -> { desc }, foreign_key: :player_id has_one :game, through: :game_player has_one :current_round, through: :game
has_many :won_games, class_name: “Game”, inverse_of: :winner has_many :player_rounds, foreign_key: :player_id has_many :rounds, through: :player_rounds [4:21 PM] 여기서는 다른 모델과의 관계를 일일이 명시했는데, omniauthable을 잘 모르겠다. include는 그냥 포함관계가 되버려서 1:다도 할수 있다. 그런데 아마도 이건 omniauth사용법에 나온대로 한게 아닐까 하는 생각이든다.
hoyoul — 11/24/2023 4:23 PM 예상과 다르다. [4:24 PM] 물론 1:다로 해도 사용하는데는 문제는 없긴하다. [4:25 PM] 그냥 user.omniauth_info.find(id: blah).email이런식으로 뭐 사용할 순 있는데…기억이 잘 안난다. [4:26 PM] 근데 중요한건, table의 field들인데…
hoyoul — 11/24/2023 4:38 PM 음…우선 내가 알고 있는 걸 정리할 필요가 있어보인다. [4:38 PM] oop에 대해서 [4:39 PM] oop를 공부하면 제일 먼저 배우는게 모든것은 객체라는 말이다. 모든것을 class로 표현하고, 그 객체를 사용해서 coding한다. [4:42 PM] 그래서 우리가 프로그래밍할 대상이 있다면 그것을 class로 만든다. 예를들어 우리가 지금 만드는 게임에 등장하는 모든것들을 class화 할수 있다. Login과정에서 내가 생각하는것은 User라는 class와 Credentials라는 class가 있다고 생각한다. [4:43 PM] User가 가질수 있는 모든 속성을 생각해서 멤버변수로 만든다. 예를들어서 email, id,pw, crendential, game room 등등등…이것을 class로 만들면 다음과 같다. [4:44 PM] class User{ string email, string name, Credentials credential, GameRoom gameroom, User(); }
hoyoul — 11/24/2023 4:45 PM 이런식으로 만든다. 물론 email도 class화 할수 있고 name도 할수 있다. 다음과 같이…. Email email, Name name, [4:46 PM] 하지만, 사용하지 않는 것까지 class로 만들진 않는다. [4:46 PM] 그래서 Email, Name은 string으로 해도 충분하다. 그 내부속성을 사용하지 않기 때문이다. [4:47 PM] 그런데 위에서 처럼 class로 만들어진 Credentials, GameRoom같은것은 그것이 가진 세부속성이 사용되기 때문이다. 이렇게 class로 만들어지면 관계가 주어진다. [4:48 PM] User는 일반화되어 있다. 즉 User라는 class는 user1, user2….user100의 instance를 일반화한것 이다. 여기서 Credentials도 마찬가지도 수만가지의 credential의 일반화한것이다. 그런데 둘의 관계는 무엇인가? has one관계다. 즉, user당 user만의 credential을 오직 한개 갖는다. [4:49 PM] 이건 어쩌면 당연하다. user하나가 여러개의 idToken을 가질순 없다. [4:51 PM] user한명은 google로 받은 유일한 token을 받기 때문에 has one관계다. 그리고 GameRoom을 보자. GameRoom은 상황에 따라 다를수 있다. 게임설계에 좌우된다. 즉 한 사용자가 여러개의 방에 들어갈수도 있다. 그러면 이경우 1:다로 설계될수 있다. 이 관계가 중요한 이유는 table에 직접적인 영향을 미치기 때문이다.
hoyoul — 11/24/2023 4:52 PM relational db를 사용하는 경우, 각각의 class는 table과 mapping이되는데 rails는 이 관계를 가지고 table을 만들어줄때 primary key foreign key로 이관계를 결정해준다. 물론 이말은 좀 모든것을 key로만 설명하지 않기 때문에 비약은 있지만, 틀린말은 아니다. [4:54 PM] 1:다 1:1처럼 두 테이블간의 관계가 설정된다는건 두 테이블에 field가 중복되지 않는다는 것을 뜻한다. [4:55 PM] 즉 User에서 있는 email필드가 Credentials table에 있을 필요가 없다는 것이다. 그 관계를 통해서해당 필드에 접근하는것이다. [4:57 PM] school이라는 table에는 student라는 필드가 student id만 잇고, school에서 학생을 찾을때는 student table에서 student id로 찾는거지. school에도 학생정보가 있고, student table에도 학생정보가 있는게 아니기 때문이다. [4:58 PM] db를 공부하면 normalization을 배우는데, 1nf,2nf,3nf같은걸 배우는데, 목적은 한가지다. 중복을 없애는 것이다. 여튼 그렇다.
11.25
son api 1.0은 oop로 작성된 program간의 통신을 매우 편리하게 작성하기위한 spec이다. json 개념 자체는 xml에서 파생된거라서 tag의 자유도가 높고, oop를 위한 직렬화를 위한 설계를 적용할 수 있다. 즉 rails에서 혁명이라고 했던 or mapping이 네트웍 너머 다른 computer에 있는 program에서도 동작할 수 있게 해주는 좋은 spec이다. 하지만, 이 spec의 전제는 oop로 작성된 program이란 전제가 있다. 지금 json api를 껍데기만 사용하려고 한다. 즉 그냥 key:value의 데이터 전송에 포커스를 두고 형식만을 이용하려는 것이다. 의미가 없는 일을 하는것이다. json api 를 구현 할려면 oop로 설계를 하던가 아니면 그냥 data를 전송하려면 data를 주고 받으면 된다. 이것도 아니고 저것도 아닌 형식만 흉내내는 것인데….우선 나는 json_api라는 package는 사용하지 않을 것이다. json을 사용하는 package는 무수히 많이 있다. 내가 자연씨의 코드에서 제일 이상했던것중 하나가, json관련해서 많은 패키지를 이용하는건데, 자연씨가 이 부분에서 혼란스러워했건 확인했다.
hoyoul — 11/25/2023 11:17 AM json을 사용하는데, json api 1.0의 형식을 사용하는건, 반드시 json_api package를 써야 하는건 아니다. 그냥 spec에 맞춰서 사용하면 되는것이다.
hoyoul — 11/25/2023 11:45 AM 근데 여기서 짜증나는건, rails의 table들이다. [11:45 AM] rails의 table은 routes의 resource와 mapping이 되고 내부적으로 객체로 처리된다. [11:46 AM] rails에서 객체들은 table의 record와 같다. 여기서 사용되는 객체들은 동일한 형태로 client에서도 구현된다. [11:47 AM] 즉 table들이 flutter에선 class로 구현되어 있어야 한다. [11:47 AM] rails의 설계를 그대로 따라가는것이다. 근데…그 설계가 이상하다. [11:48 AM] json 1.0이란건 rails에서 사용되는 객체들이 직렬화를 거쳐서 client에서도 객체로 똑같이 1:1 mapping되는 것을 편하게 해주려고 만든것이다. [11:49 AM] 우선 user를 보자.
이거대로 class를 만들면 다음과 같다.
[11:50 AM]
cards는 list로 전달할려는 거 같다.
[11:51 AM]
정확한 내용은 모르지만, card는 class로 만들고 rails에선 저렇게 할려면 table을 만드는게 맞다. 그리고 관계를 설정해야한다.
hoyoul — 11/25/2023 11:52 AM card는 게임의 핵심 class의 하나기 때문이다. email이나 name처럼 핵심적 요소가 아니면 string이나 list로 전달해도 상관이 없다. (edited) [11:58 AM] 아..user를 먼저 보면 안된다. [11:59 AM] 왜냐 user에는 credentialID같은게 없다. 즉 user와 tokens들 사이에 관계가 없기 때문이다. [11:59 AM] 이 관계가 빠졌다. User에는, 사용자들이 login시 google로부터 얻게되는 token이 있어야 하는데, 없다는 것이다. (edited)
hoyoul — 11/25/2023 12:00 PM 별도로 있는데, 우선 내가 하는건 token들을 나타내는 class를 만들어서 json으로 보내야 하기때문에 omminAuth를 class로 만들자. (edited) [12:03 PM]

Figure 76: rails2
그런데 여기에 token이 있는지 없는지 모르겠다. user_id가 token이라고 예상되는데… [12:05 PM] 아…이 부분이 관계설정이, User에서 omniAuth를 has one으로 가지고 있는게 아니라 omniAuth에서 User를 has one혹은 has many로 가지고 있기 때문에 foreign key로 user_id가 필드에 있는듯 하다.
hoyoul — 11/25/2023 12:13 PM 이렇게 하는건, rails가 알아서 그냥 보내줄께, client는 알아서 처리하라 구조인데… [12:14 PM] token이 휘발성이니까 db에 저장할 필요가 없어서라고 말할수도 있다. [12:15 PM] 아..좀 힘들다. [12:19 PM] token이 redis에 있을수도 있다. 그러면 token에 해당하는건 model을 만들필요도 없고, User의 속성으로 credential이 있을 필요도 없다.
hoyoul — 11/25/2023 12:26 PM omniAuth에 token이 없으면 omniAuth는 뭐지? User도 아니고, User정보인 name과 email photo가 있으면 User라고 생각할텐데..OmniAuth가 User인가? [12:27 PM] 어떻게 짜야할지 감이 안온다. [12:29 PM] 순상님한테 도움을 요청해야겠다.ㅋ
hoyoul — 11/25/2023 1:29 PM 인증과정은 websocket을 사용할 수 없다고 했다. 즉 rails에서 actioncable로 인증부분은 처리가 안되어 있다고 보면된다. [1:29 PM] 그러면 rest api를 사용해서 인증을 처리해보자.
hoyoul — 11/25/2023 2:02 PM 근데, 인증과정에서 얻어온 emil과 exp나 idToken은 어떻게 유지를 하라는건가? [2:03 PM] 이게 c언어면 static string email이나 static string idToken을 가지고 처리한다고 하지만, [2:04 PM] oop로 되어 있는코드에서, 받은 데이터들은 user가 계속 가지고 있어야 하는 속성인데… (edited) [2:05 PM] 한번쓰고 없어지는게 아닌데… [2:06 PM] expired time이란것은 user객체를 새로 생성하라는 의미라서 user객체의 속성이다. [2:06 PM] 그리고 user의 id를 나타낼려고 email, idToken을 사용한다면 이것또한 user객체의 속성이다.
hoyoul — 11/25/2023 6:57 PM [중요] ——————설계과정에 대해서———– [7:02 PM] 내가 이걸 쓰는 이유는 내가 생각하고 배워왔던것과 다른 설계에 맞닥뜨렸을때 내가 어떻게 해결해야 하는지 고민한것을 기록으로 남겨 다시 시행착오를 겪지 않으려 한다.
hoyoul — 11/25/2023 7:09 PM oop로 설계시, [7:10 PM] oop로 program을 설계할때는 제일 먼저 program에 필요한 요소들을 선정해야 한다. 그리고 그들관의 관계를 설정한다. 그게 UML로 작성이 된다.
hoyoul — 11/25/2023 7:18 PM 처음에는 front-end, back-end를 생각하면서 설계를 하지 않는다. [7:19 PM] 하나의 program에서 동작한다고 생각하고 짜는 것이다. [7:20 PM] 제일 먼저 하는것은 회원가입, 그다음 login이다. 물론 관리자 페이지 부터 만드는 회사들도 있다. 나는 그 방법도 일리가 있지만, 일반적인 방법론을 말하는게 나을 듯하다. [7:22 PM] 회원가입과 login은 User라는 class에 대한 내용이다. (edited)
hoyoul — 11/25/2023 7:28 PM 먼저 회원가입을 보자. 회원가입에 사용되는 User라는 class는 처음 UML을 만들때, 필요한 property들을 이미 작성했었다. 회원가입 폼으로 부터 property를 입력을 받아서 User instance를 생성하고, db에 저장하는 작업을 한다. google oauth를 사용하는 경우, 이것이 회원가입에 해당된다. 처음이라, user instance를 db에 넣기 때문이다. (edited) [7:32 PM] 그러면 User에 대한 instance가 만들어졌다. 여기에는 User에 대한 name, id,pw, 등등등 User에 대한 세부내용이 채워진 instance다. 즉 회원가입이 하는것은 User를 db에 저장, User instance의 생성이다. (edited) [7:34 PM] 여기엔 이미 회원이 db에 있는지 없는지 여부라던지, 회원 form에서 user의 property값설정 누락이라던지, oauth의 실패로 인한 처리라던지 그런것들이 수행되야 한다. [7:35 PM] stand-alone program에서의 회원가입은 동일하게 back-end, front-end로 나눠질 수 있다.
hoyoul — 11/25/2023 7:36 PM back-end와 front-end는 mvc분리의 의미도 있지만, 예전 처음 생각은 물리적인 분리에 따른 것이였다. mvc때문에 front-end와 back-end로 나눈게 아니라, network이라는 물리적 구분에 의해서 분리되었다. [7:38 PM] User라는 instance가 물리적으로 떨어진 front-end, back-end로 나눠지면, front-end에도 back-end에도 동일한 User instance가 존재해야 하고, 이를 db에도 있어야 한다. 이런 issue에 대해서 여러기술들이 나오게 되었다. [7:41 PM] User instance와 db의 consistency를 위해서 OR mapping이란 기술이 사용되고 그리고 front-end와 back-end의 instance constistency를 위해서 객체직렬화, RMI도 나왔다. 이런것들은 20년전 기술이다. 새로운게 아니다. 예전부터 있었고 예전부터 사용되었고 지금도 사용되는 것이다. [7:43 PM] 그럼 여기서 back-end와 front-end의 user instance는 consistent해야 한다.그렇게 하기위해서 backend에서 정의된 db table, class가 front-end에도 동일하게 구성되어야 한다.
hoyoul — 11/25/2023 8:20 PM backend와 frontend는 서로 동일한 객체를 가져야 하는데, 회원가입에 해당하는 google oauth를 하면 무엇을 해야 하냐면, name, email, photo, idToken, accessToken(만료시간저장) 같은 데이터를 얻어서 User라는 객체 instance를 만들고 이를 db에 저장하고, front-end에 전달해 주어야 한다. (edited) [8:23 PM] 그래서 front-end는 동일한 객체정보를 유지한다. 그런데 여기서 이렇게 처리를 안하는 경우 어떻게 해야 하는가? [8:24 PM] 예를들어서, google oauth를 통해서 얻은 정보중 email과 id토큰만 주고, 나중에 또 photo, name, balance 같은 정보를 준다면? [8:25 PM] 객체직렬화를 사용하는 json api를 사용하면 안된다. 그리고 User객체 초기화를 할수 없다. [8:25 PM] 그러면 어떻게 해야 하는가? 가 내 고민이다. 첫번째 방법은 임의의 class를 만드는 것이다. [8:27 PM] 임의의 class에선 첫번째 통신으로 얻는 data를 받아서 저장하고 있어야 한다. 그리고 User class의 멤버변수는 late나 null safety처리를 해서 받은 통신데이터로 일부만 초기화한다. (edited)
hoyoul — 11/25/2023 8:28 PM 진짜 복잡하게 짜는거다. [8:31 PM] 이런식으로 일을 하면, 매일 야근에 매일 주말에 일해도 진도는 안나가는 상황이 된다. [8:33 PM] 원래 스타트업은 잘하는 몇명이 뚝딱뚝딱해서 만드는건데, 막짜는게 아니라 이런걸 기본으로 생각하고 다 기술적으로 지원되기 때문에 때문에 뚝딱뚝딱이 되는건데… [8:34 PM] 이건 시간을 투자하면서 욕을 먹는 아이러니가 된다.
hoyoul — 11/25/2023 9:01 PM 여튼 임의의 class를 만들기로 한다. util이라는 폴더를 만들고, 여기에 static method를 만든다. A라고 하자. A에 google로 받은 인자를 전달한다. 3개의 token이다. 그리고 3개의 token을 받은 A함수는 json을 만들어 보낸다. 그리고 return받은것을 처리해야 하는데, return받은걸 어떻게 처리할 지는 좀 생각하자. 보내고 제대로된 응답만 확인하자. 진짜 이렇게 하면 수습이 안되는거 알지만, 내 능력의 한계다. [9:07 PM] OauthHandler? OauthService란 class를 만들자. [9:08 PM] webSocket을 처리하는 service가 있기 때문에 OauthService가 이름이 더 좋아 보인다.
hoyoul — 11/25/2023 9:12 PM userController에서 하는 일들을 oauth로 넘기자. 왜냐면 이런식에선 obx 사용이 의미가 없다. UserModel을 사용도 하지 않기 때문에 controller에서 처리하는게 이상하다. [9:12 PM] OauthService [9:12 PM] static으로 할필요는 없다. 그냥 method를 사용해도 된다. [9:17 PM] 야식시켜먹고 좀 쉬고 하자.
hoyoul — 11/25/2023 9:29 PM dart나 flutter yasnippet이 있는지 확인해보자.
hoyoul — 11/25/2023 10:05 PM oauth service는 json처리와 연결작업을 모아놨다. getXcontroller에서 service객체를 만들어서 메소드를 호출하는 방식으로 할것이다. controller에서 바로 처리하는게 아니라고 생각해서 쓰리쿠션이다. 웃기긴 한데, login버튼 누르면 controller에서 oauth service를 이용하는 식이다. [10:11 PM] 왜냐면 oauth는 model을 온전히 사용하지 않는데, controller에서 처리하면 안되기 때문이다. mvc는 원래 한몸이라서 그렇다. [10:11 PM] 그래서 별도 처리하고 controller에서 호출하는식으로 하는게 맞아 보인다.
hoyoul — 11/25/2023 10:29 PM 인증에서 web socket을 사용하지 않으니까, http 통신하니까. dart convert와 http내장 package를 사용하던가, dio를 쓰면 통신하고 에러가 나면 http니까 response code로 판단하면 되는데… [10:31 PM] dart convert는 어차피 필수고 왜냐면 json을 convert해야 하니까…그러면 http라는 내장 패키지로 통신하느냐? 아니면 dio로 통신하느냐 차이가 있다. [10:32 PM] 상관은 없는데, 외부패키지던 내부 라이브러리던, 뭐 더 좋으니까 외부 package사용하는거니까…dio를 설치해서 사용하자. 대충 읽어보자. [10:35 PM] 특이하게 dart pub add dio, flutter pub add dio 두개 설치가 나와 있다. [10:35 PM] 두개를 다 설치하냐…그럴리는 없다. flutter pub add dio만 하면될듯하다.
hoyoul — 11/25/2023 10:44 PM import ‘package:dio/dio.dart’;
hoyoul — 11/25/2023 11:35 PM import ‘package:google_sign_in/google_sign_in.dart’; import ‘package:dio/dio.dart’; import ‘dart:convert’;
class OauthService {
Future<Map<String, dynamic>?> fetchTokenFromGoogle() async { final GoogleSignIn googleSignIn = GoogleSignIn( scopes: [’email’], ); GoogleSignInAccount? account = await googleSignIn.signIn();
if (account != null) { var auth = await account .authentication; /accessToken,idToken cannot be picked in account. Map<String, dynamic> tokens = { ‘accessToken’: auth.accessToken, ‘idToken’: auth.idToken, ‘serverAuthCode’: account.serverAuthCode, }; return tokens; } return null; } / test function void relayGoogleToken() async { Map<String, dynamic>? googleTokens = await fetchTokenFromGoogle(); if (googleTokens != null) { sendTokenToRailsServer(googleTokens); } else { print(‘Google Sign-In was canceled or failed.’); } } Future<void> sendTokenToRailsServer(Map<String, dynamic>? tokens) async { var dio = Dio(); if (tokens != null) { var accessToken = tokens[‘accessToken’]; var idToken = tokens[‘idToken’]; var serverAuthCode = tokens[‘serverAuthCode’]; try { var response = await dio.post( ‘https://https://kholdem.fly.dev/auth/google_oauth2_with_id_token ’, data: jsonEncode({ “omniauth.auth”: { “credentials”: {“token”: accessToken}, “extra”: {“id_token”: idToken}, “server_auth_code”: serverAuthCode, } }), options: Options(headers: { “Content-Type”: “application/json”, }));
} [11:35 PM] if (response.statusCode == 200) { print(‘success.’); print(‘response data: ${response.data}’); } else { print(‘failed: ${response.statusCode}’); } } catch (error) { print(’error: $error’); } } } [11:35 PM] 테스트로 만들어봤다. [11:39 PM] 테스트는 아직 안해봤는데, 잘수도 있어서…미리 써놓는다. 2개의 함수를 만들었다. 하나는 google값을 가져오고 다른 하나는 rails server에 요청하는데, dio를 사용했고, google_oauth2_with_id_token을 post로 요청했는데 이건 자연씨가 그렇게 사용했기때문에 이용했다. relay google token이란 함수를 만들었는데, test용이다. 즉 google에서 받은토큰을 rails로 연계하는 test function이다. controller에서 이것을 호출해서 return받아서 사용할 예정이다. November 26, 2023
hoyoul — 11/26/2023 12:10 AM Failed to build iOS app Error (Xcode): Invalid depfile: Users/hoyoul/Development/Projects/kholdem-front.dart_tool/flutter_build/533015 d1dec3077298904e5391f1db57/kernel_snapshot.d [12:10 AM] 빌드하니 갑자기 이런에러가 나온다. [12:11 AM] flutter clean, flutter pub get을 해보자. [12:12 AM] Launching lib/main.dart on iPhone 15 in debug mode… Running pod install… 1,043ms Running Xcode build… Xcode build done. 13.6s Failed to build iOS app Error (Xcode): ../../../.pub-cache/hosted/pub.dev/js-0.6.7/lib/js.dart:10:1: Error: Dart library ‘dart:js’ is not available on this platform. [12:12 AM] 이런 에러가 나온다. [12:14 AM] flutter doctor는 이상없다. [12:15 AM] 난 dart.js를 사용하지 않는데…
hoyoul — 11/26/2023 12:23 AM 안된다… [12:23 AM] branch 하나 만들고 reset hard로 head를 한칸 옮기고 diff작업해야 할듯하다.
hoyoul — 11/26/2023 12:43 AM flutter clean하고, pub-cache를 지우고 다시 실행해보자. [12:44 AM] flutter run을 해보자. [12:45 AM] 동일 에러가 발생한다. [12:45 AM] Failed to build iOS app Error (Xcode): ../../../.pub-cache/hosted/pub.dev/js-0.6.7/lib/js.dart:10:1: Error: Dart library ‘dart:js’ is not available on this platform.
hoyoul — 11/26/2023 1:24 AM 우선 이전 commit으로 이동했고 동작 테스트를 해야할듯하다.

Figure 77: rails3
에러가 났다. debugging하니까, dio에서 socket에러다. [2:03 AM] host lookup error인데 [2:04 AM] 주소에 문제가 있는듯 하다. postman으로 제대로 되는지 확인해보고 주소가 틀렸다면 변경하자.
hoyoul — 11/26/2023 2:24 AM 주소에 오타가 있었다.

Figure 78: rails4
rails하고 교신했고, 데이터를 가져왔다. 그다음은 이 token을 bearer로 보내서 game정보를 가져와서 처리하고 뭐 할텐데…그전에 controller로 page이동도 해야 한다. [2:25 AM] 오늘은 여기까지만 하자.
hoyoul — 11/26/2023 2:32 AM git에 반영했다.
hoyoul — 11/26/2023 2:46 AM postman에 보니까, websocket하고 rest api가 나눠져서 되어 있다. 그냥 그런생각이 들었다. flutter에서 상태관리를 하는건 많다. 여기서 bloc을 쓰던, getX를 쓰던 provider를 쓰던 이건 아무거나 선택해도 상관없다. 그리고 ui layout도 constraints layout을 사용하던 mediaquery를 사용하던 아무거나 사용해도 상관없다. package를 dart의 내장 library를 사용하던 외장을 사용하던 상관없다. 문제가 되는건 두개를 짬뽕해서 사용하는게 문제가 되는거다. http를 사용하던 websocket을 사용하던 하나만 사용하는게 맞다.
hoyoul — 11/26/2023 3:00 AM 연결시에 serverAuth code가 null이다. 왜 null일까? 이거 local로 테스트할때는 아니였다. null값이 아니였다. 내생각에 scope에서 email만 받기때문에 그렇다고 생각한다. 즉, 처음 gcp설정할때 scope를 설정을 다르게 해야한다. 왜냐면 난 scope를 몰라서 찾아보고 했다. 여기서 여러개를 test를 했었는데, 그때 당시는 신경을 별로 안썼는데 여튼 이것과 관련되어 있을수도 있다. 내가만든 gcp도 있으니까 테스트해볼수는 있는데, 지금 작성한 코드의 변경이 필요해서 모르겠다…
hoyoul — 11/26/2023 3:08 AM 테스트할려면 하는데, googleservice-info.plist를 다시 다운받아서 설정해야 내가만든 gcp로 접속하고 그다음 serverAuth를 checking할 수 있긴 하다. 그런데 discord를 내가 emacs keybinding을 했었나보다. keybinding이 먹힌다. [3:09 AM] 좀 신기하다.ㅋ
11.26
Auto login [10:19 AM] 한번 login이 성공한 경험이 있다면, 다음 앱에 접속할때, 로그인이 필요하지 않게 처리하려면 shared preference를 사용해야 한다. [10:20 AM] 난 google에서 oauth를 가져오는것이 회원가입 + login이라고 생각한다. 회원가입이라고 생각하는 이유는 db에 계정정보를 저장하기 때문이다. login이라고 생각하는건 User객체가 만들어지기 때문이다. 우리앱은 좀 다르지만, 여튼 그렇다. (edited) [10:24 AM] 회원가입과 login을 나눠서 처리할려면, 사용자 입력 form과 회원가입 form을 만들어야 한다. google oauth를 사용해서 회원가입과 login을 동시에 하고 있기 때문에 한번 정상적인 google login이 되었다면 이 정보를 shared preference에 저장한다. app에 접속할때마다, shared preference에 데이터가 있다면 login버튼을 보여주지 않고, game화면으로 진입해야 한다. (edited)
hoyoul — 11/26/2023 10:28 AM https://pub.dev/packages/shared_preferences flutter pub add shared_preferences
hoyoul — 11/26/2023 10:41 AM 목욕탕가고 방청소 하고 가능하면 머리깎고 저녁먹고 일하자. November 27, 2023
11.27
server에서 받은 token과 email을 저장한다. [10:12 AM] shared preference에서 login이 성공하면 저장한다. [10:13 AM] [login 과정에서 문제점] [10:13 AM] (1) flutter에서 serverAuth 값을 null로 전송한다. (edited)
hoyoul — 11/27/2023 10:21 AM => GCP에서 consent screen을 작성할때 scope 처리나 설정에서 처리가 안된듯 하다. google에서 serverAuth를 받아서 이것을 rails 서버에 전달하고 rails서버에서 google에 severAuhtcode로 접속해서 access token을 받아서 처리하는데, 이부분이 안되어 있다. access token는 만료일이 포함되어 있다. 그래서 rails에서 google에서 받은 ccesstoken에서 만료일을 꺼내서 flutter에게 return하거나 accessToken를 리턴해야 한다. 지금은 위와 같은 과정을 거치지 않고 rails에서 token과 email exp를 flutter로 전달한다.
(2) name을 서버로 부터 전달받지 못한다. (edited) [10:23 AM] shared preference 작성 [10:24 AM] 서버로 받은 token과 email, exp를 처리해야 한다.
hoyoul — 11/27/2023 10:38 AM success 했을때, 받은것을 response로 처리가능한지 보자. [10:40 AM] debugger에서 response.data의 값을 eval할수 있는지 확인해보자.

Figure 79: debug2
JsonWebToken이란 객체의 4개의 속성값을 넣어보낸다. Json1.0은 원래 객체를 명시적으로 보내기 위한 형식으로 data key, 그리고 class에 해당하는 type, attribute에 해당하는 멤버변수를 넣어보내기 때문에, 객체를 처리할수 있는 rest_api package나 json_api라는 package를 사용하는게 편하다. [11:14 AM] 나는 data:convert와 dio를 사용한다. dio를 사용해서 데이터를 가져오는 통신을 할 수 있고, json을 data로 추출하는것은 dart:convert를 사용했지만, 아무래도 객체를 다루는데는 외부 package를 사용하는게 더 좋다. 왜냐면 dart convert는 객체 단위가 아니다. [11:15 AM] jsonAPI나 rest api패키지 어느것을 써도 상관없다.
hoyoul — 11/27/2023 11:35 AM json Api 1.0을 쓴다는것은 받는것도 보내는것도 객체형식이다. 그런데, rails에선 요청을 받는건 객체형식이 아니고, 보내는건 객체형식이다. [11:36 AM] 그러면 client에선 dio를 써서 요청하고 받는건 jsonapi나 rest api를 섞어 쓰게 된다. 그래서 자연님 코드를 알아볼수 없던것도 있다. [11:37 AM] 여기서 고민이 된다. [11:38 AM] 나도 dio, dartconvert를 쓰면서 json_api를 같이 써야 하는가? [11:40 AM] 우선 그냥 수동으로 까자. 즉 response는 어차피 map형태이기 때문에 [11:41 AM] extractDataFromResponse()를 만들고 여기서 data를 꺼내자.
hoyoul — 11/27/2023 11:46 AM 구조를 보면, data: map 으로 되어 있고, map안에 type: value가 있고, attribute: map이 있다. [11:47 AM] 두개의 map을 만들고, map에서 data를 꺼내면 되지 않을까? [11:50 AM] bp걸고 테스트 해보자. [11:52 AM] 음..에러가 난다.
hoyoul — 11/27/2023 12:02 PM 고쳤다. 제대로 가져오게 했다. 이렇게 하면 jsonAPI나 rest_api package를 사용하지 않고 옛날 방식으로 처리하는거지만, 어쨋든 복잡하진 않다. [12:06 PM] 즉 정리하면 난 class를 만들고 class로 처리하지 않는다.
hoyoul — 11/27/2023 12:10 PM 근데 보내주는 data중에 debug라는 건 왜 보내는 건지 모르겠다.
hoyoul — 11/27/2023 12:18 PM exp를 현재 시간으로 나타내서 difference함수를 사용하면 기간을 human-friendly하게 바꿀수 있을듯하다.

Figure 80: exp1
rails 서버로 부터 받은 만료일은 만료일이 아니다. [12:33 PM] 현재 시간이다. [12:34 PM] => 아니다. 만료일 맞다. [12:35 PM] 2024년으로 되어 있기 때문에 1년간의 만료일을 준것이다. [12:35 PM] 이제 받은 값들을 shared 에 저장해야 한다. [12:36 PM] 받은값들은 email, token, exp만 넣으면 될듯하다. [12:38 PM] shared preference를 좀 읽어봐야 할듯하다. 어제 읽었어야 하는데…여튼 다시 읽어보자. [12:39 PM] https://pub.dev/packages/shared_preferences
flutter pub add shared_preferences
hoyoul — 11/27/2023 12:39 PM 설치한다. [12:44 PM] Shared Preference [12:45 PM] iphone이나 android device에서 사용되는 hash table형태의 storage공간이다. [12:45 PM] 즉 file system이나 db로 보면된다. 영구적으로 저장되는 것이다.
hoyoul — 11/27/2023 12:46 PM 사용할려면, c처럼 file handle을 가져와서 사용하면 된다. 이것도 비동기다. cpu+memory외에는 비동기 통신이라고 보면된다. [12:48 PM] class SharedPreferencesDemoState extends State<SharedPreferencesDemo> { final Future<SharedPreferences> _prefs = SharedPreferences.getInstance(); late Future<int> _counter;
Future<void> _incrementCounter() async { final SharedPreferences prefs = await _prefs; final int counter = (prefs.getInt(‘counter’) ?? 0) + 1;
setState(() {
\_counter = prefs.setInt('counter', counter).then((bool success) {
return counter;
});
});
} [12:49 PM] 예제에 있는 코드인데, getInstance()라는 static method로 instance를 가져오는게 보인다. 이런식으로 가져오는 방식은 전형적인 factory pattern이고, 내부에 singleton을 사용해서 오직 한개의 instance만 사용하는 구조로 되어 있을꺼라고 예상이 된다. 즉 pool을 가지고 있을 것이다. [12:49 PM] java에서 사용하는 형태다. [12:50 PM] getInstance로 instance를 가져왔으면 저장은 어떻게 하나? 그리고 다시 어떻게 꺼내 쓰나? 이건 예제를 봐도 되고, 아마 자연씨가 사용한게 있을거다. 이거 참조 해도 된다. [12:51 PM] 내가 shared reference를 사용해야겠다고 생각한게 아니라, 자연씨 코드 분석시 shared reference를 사용한 코드를 봤기때문에 검색해서, 아 auto login한거구나를 알았기 때문이다. [12:52 PM] 밥먹고 와서 하자.
hoyoul — 11/27/2023 1:12 PM 근데, 나는 login과정에서 rails가 return하는 user정보중에 balance정보도 줘야 한다고 생각한다. 돈이 있어야 게임할꺼 아닌가, name하고 balance를 받기 위해 또한번 통신을 해야 하는건 아닌거 같다. rails에서 token을 만들어서 return할때, JsonWebToken이란 객체를 돌려주는게 아니라, 저번에도 썼듯이 User란 객체에 jsonWebToken을 include로 보내던가, 아니면 attribute에 포함시켜서 보내는 것이다. (edited) [1:13 PM] 그러면 user가 만들어지고, user가 만들어진후 gameroom list를 server에 요청해서 방리스트를 가져오는것이다. [1:14 PM] game room list는 client에서 유지하면 안된다. 왜냐하면 game room list는 client에서 처리할수 없기 때문이다. 만일 client app에서 game room list를 운영하면 app을 종료하면 다른 사람은 game을 할 수 없다. 그래서 server에서 game room list를 유지해야 한다. 물론 db에 유지하는건 아니고 mongo db사용하는것처럼 redis에서 처리하면된다. (edited) [1:15 PM] rails에서 game room은 처음에는 없을수도 있다.game room을 만드는것은 client에서 요청해서 만들수도 있다. (edited) [1:16 PM] client에서 game room list가 아무것도 없다면 create game room해서 rails에 요청해서 방을 만들고 들어가던가, rails로부터 받은 game room list가 empty가 아니라면 두개의 option이 있다. create room으로 만들고 들어가던가 선택해서 들어가던가… (edited) [1:18 PM] 여튼 갑자기 생각나서 쓴다.
hoyoul — 11/27/2023 1:21 PM shared preference [continued…] (edited) [1:23 PM] 여튼 다시 이어서 하면
hoyoul — 11/27/2023 1:43 PM sharedPreference에 저장하는 법 [1:44 PM] final SharedPreferences prefs = await SharedPreferences.getInstance(); await prefs.setString(’email’, email); await prefs.setString(‘idToken’, idToken); [1:44 PM] instance를 얻어와서 set으로 저장하면 된다. (edited) [1:45 PM] sharedPreference에서 값 꺼내오는 법 [1:47 PM] final SharedPreferences prefs = await SharedPreferences.getInstance(); String? email = prefs.getString(’email’); String? idToken = prefs.getString(‘idToken’); (edited) [1:47 PM] instance를 얻어와서 get으로 가져오면 된다. [1:49 PM] 가져올때 null값일수도 있으니, String이 아니라, String? 으로 바꾸고, null이 아닌경우만 처리하는 if문을 사용해서 처리해야 한다.
hoyoul — 11/27/2023 1:50 PM 만기일도 저장해야한다 왜냐면 처음 shared preference에서 login정보가 있다면, rails에 연결하지 않을것이기 때문이다.
hoyoul — 11/27/2023 1:58 PM 근데, context action에서 organize imports는 동작 안되는건가?
hoyoul — 11/27/2023 2:10 PM 10분간 쉬자.
hoyoul — 11/27/2023 2:49 PM oauthService에서 사용하던 함수들을 controller로 옮길때 Response를 그대로 return하게 했을때 에러가 난다. dio의 Response와 충돌이 난다. 그래서 as dio로 namespace를 줬는데, 이렇게 하면 좀 가독성이 떨어지는데, 우선 동작은 되는데, 좀 생각은 해야할 듯하다. [2:50 PM] controller와 service의 경계를 어떻게 나눌것인가?
hoyoul — 11/27/2023 2:58 PM 왜 내가 controller 로 옮겼냐면, token및 email정보들이 User의 일부로 봤다. 따라서 UserController에서 처리하는게 맞다고 생각했다. oauthService는 연결과 같은일만 하고 실제 데이터 처리는 controller가 해야하는게 맞다고 생각했기 때문이다. [3:01 PM] 그런데 rails에서 User를 가져오지 않기 때문에, User model은 만들지 않고, controller의 member 변수로 처리하는걸로 우선 하자.
hoyoul — 11/27/2023 3:24 PM Future<void> executeOauth() async { OauthService oauthService = OauthService(); dio.Response response = await oauthService.relayGoogleToken(); extractDataFromResponse(response.data); insertDataIntoSharedPreferences(); [3:25 PM] controller에서 execvuteOauth를 수행하면, service를 통해서 google,rails를 거쳐 response data를 가져온다. 그리고 그것을 extract해서 꺼내서 member변수에 세팅한다. 이렇게 하는 이유는 obx를 사용할 수도 있기 때문이다. [3:26 PM] 그리고 member변수를 shared preference에 저장한다. [3:26 PM] 이제 checkLogin함수를 만들것이다. [3:26 PM] checkLogin() or isLogin()는 사용자가 처음 login하는지 안하는지를 check한다. [3:27 PM] 기준은 shared Preference다. shared preference에 data가 있다면 처음이 아니다. [3:28 PM] data가 없다면, executeOauth()를 호출해서 인증하고 shared preference에 저장한다. [3:29 PM] data가 있다면 getX()의 routing을 사용해서 gamelobby로 가야하는데… [3:30 PM] game list들이 있는 방, 그리고 game room을 선택하는 화면은 없다. [3:30 PM] 그리고 사용자명도 없다.
hoyoul — 11/27/2023 3:32 PM shared preference에 데이터가 있다면, 방정보를 얻는 rest api나 websocket을 사용해서 가져와야 한다는 거 같은데, 보면 bearer를 사용하는건 restapi였다. 즉 post로 보낼때, 받은 id token을 다시 보낸다는 건데… [3:32 PM] websocket으로 된건 어디서 부터 어디고 rest api로 된건 어디서부터인지는 모르겠다. [3:32 PM] 우선 checkLogin()부터 만들자.
hoyoul — 11/27/2023 3:47 PM checkLogin을 테스트해보자.
</img/oauth/checklogin1.mp4>
한번이라도 login이 성공했으면 바로 intro page로 간다. 난 이미 테스트했기 때문에 login하지 않고 바로 intro page로 간다. 가기전에 필요한 데이터가 있다..
hoyoul — 11/27/2023 3:55 PM 우선 git에 반영부터 하자. [3:57 PM] preference지우고 다시 물어보는지 체크 한번 더하자
</img/oauth/checklogin2.mp4>
약간의 문제가 있다. 비동기 처리가 안되고 있다. await가 안되는거 같다. [4:08 PM] 이전 화면을 유지하고 google auth가 동작해야 하는데, 미리 gameIntropage가 보여진다는건 game intropage가 await안된다는건데… [4:09 PM] 처리했다. 비동기문제가 맞다. [4:10 PM] 10분간 쉬자. [4:13 PM] git 반영했다.
hoyoul — 11/27/2023 4:24 PM introPage는 원래는 gameLobby page였다. 여기서 원래 gameroom list를 나타내고 game방을 만들어야 한다. 그런데 game방은 하나로 가정한다. [4:25 PM] 그래서 3 waiting other players, 2 seat란 값을 보여줘야 하는데, [4:25 PM] 이것도 oop로 처리가 되야 하는데, 이것도 아마 그냥 data를 가져와서 보여주는식이 될거같다. [4:26 PM] 모르겠다. 이렇게 짜면 안되는걸 알지만…서버에서 정보를 가져오는 api가 뭔질 알아야 한다. [4:26 PM] 이건 자연씨 소스에도 없다. 왜냐 처리를 안했기 때문이다. [4:28 PM] 우선 bearer 날려서 gameroomlist인가, 그걸 가져오는 부분이 있으니까, 쓸만한 정보가 있는지 살펴보자. [4:30 PM] grep으로 보면 bearer를 사용하는게 2개 나온다. [4:31 PM] 여기선 rest api package를 사용한다.
hoyoul — 11/27/2023 4:34 PM dio.options.baseUrl = “https://kholdem.fly.dev/api/v1 ”; dio.options.contentType = “application/json”; dio.options.headers[“Authorization”] = “Bearer ${SettingController.to.userToken}”; return dio; }
static Adapter _adapterInstance() { return JsonApiAdapter(‘kholdem.fly.dev’, ‘/api/v1’) ..addHeader(“Authorization”, “Bearer ${SettingController.to.userToken}”); } [4:35 PM] 자연씨 코드에 보면 같은 주소를 하나는 dio를 사용해서 http통신하는 부분이 나오고, 다른 하나는 adapter를 써서 http 통신하는 부분이 나온다. adapter를 사용한다는것은 rest api패키지를 사용하는 것이다. 그리고 gamedata는 web socket을 사용하는거 같다. [4:36 PM] 이렇게 섞어쓰는 이유를 모르겠다. [4:36 PM] dio를 쓴건 처음 인증이고, 두번째는 bearer가 있는건 room list를 받아오나보다. [4:38 PM] 아니다 둘다 bearer가 있다. 그래서 처음 dio가 인증은 아니란거다. [4:39 PM] 근데 저주소로 token값만 주면 room list가 나오긴 하나보다. 한번 해보면 되는데..

Figure 83: gameroom1
gameroooms라는 uri가 있는데, 자연씨 코드에선 이렇게 접근하지 않는다. [4:48 PM] postman에서 header에 bearer붙이는 법을 모르겠다. header에 key를 뭘로 주고, 값이야 idToken, rails에선 authToken이라는거 주면될텐데… [4:48 PM] rails 코드를 좀 봐야겠다.

Figure 84: gameroom2
game_rooms를 resources로 했고, index만 열어놨다. [4:51 PM] 요청하면 json으로 index를 return한다고 한다. [4:51 PM] emacs에서 rails를 세팅하지 않아서 이동이 불편하다.
hoyoul — 11/27/2023 4:53 PM 여튼 gamerooms라는 controller에 가보자. [4:54 PM] gameRooms를 다 return하는데, 지금의 구조와는 또 안맞다. 왜냐 [4:54 PM] 지금은 gameroom이 하나다. [4:54 PM] 그러면 다 받아와서 [0]번째만 사용해도 되지만, [4:55 PM] class Api::V1::GameRoomsController < Api::V1::BaseController def index game_rooms = GameRoom.all options = { include: [:current_game, :users] } render json: GameRoomSerializer .new(game_rooms, options) .serializable_hash.to_json end [4:56 PM] 코드는 이런식인데, [4:56 PM] GameRoom의 모든 record를 gameroom로 넣고 option이 있는데, 이게 좀…이상하다.. [4:57 PM] current_game과 users라는 모델이 연관모델이라서 같이 return할껀데 [4:57 PM] 여기서 included도 json api에서 말하는 included와 같은데… [4:57 PM] 그러면 GameRoom에 currentGame하고 User가 멤버변수로 봐도되는데…한번 record를 봐보자.

Figure 85: gameroom3
state하나다. [4:59 PM] 이건 좀 이상하다. [4:59 PM] currentUser는 model도 아니다. [4:59 PM] currentGame.
hoyoul — 11/27/2023 5:00 PM User의 id가 field에 없는데 included를 어떻게 하지… [5:00 PM] 밥먹고 와서 보자.
hoyoul — 11/27/2023 7:37 PM rails는 다 생략되어있다. 그래서 엄청 빠르게 개발할 수 있다. 시간될때 branch하나 만들고 작업해야겠다. [7:38 PM] websocket 테스트나 해보자. [7:38 PM] rails에서 어떻게 되어있는지 한번 보자. [7:39 PM] git pull하니 update가 되어 있다. [7:40 PM] channel 소스를 보자. [7:40 PM]
class GameRoomChannel < ApplicationCable::Channel rescue_from StandardError, with: :deliver_error_message
ONLY_GAME_ROOM_ID = “GameRoom-1”
def join puts “channel: join” @game_room = GameRoom.first stream_from(ONLY_GAME_ROOM_ID) end
def subscribed puts “channel: subscribed” stream_from(ONLY_GAME_ROOM_ID) end
def subscribe puts “channel: subscribe” stream_from(ONLY_GAME_ROOM_ID) end
def leave stop_stream_from(ONLY_GAME_ROOM_ID) @game_room = nil end
def start
@game_room.game.start end
def die end
def kall user = User.find(params[:user]) user.kall end
def half end
private def deliver_error_message(e)
end
end
hoyoul — 11/27/2023 7:44 PM socket을 사용해서 접근할려면 socket주소가 있어야 한다. [7:45 PM] postman에 보니까. ws://localhost:3000/websocket이 있다. [7:49 PM] 찾았다. final wsUrl = Uri.parse(‘ws://kholdem.fly.dev/websocket’);
hoyoul — 11/27/2023 7:53 PM 근데 소스는 잘 모르겠다. only_game_roomID는 그냥 문자열인데… [7:56 PM] channel과 action들을 인자로 줘야 하는지는 모르겠다. [7:58 PM] 10분간 휴식
hoyoul — 11/27/2023 8:18 PM login button을 누르면 websocket연결하는 test를 해보자. [8:23 PM] bp 걸고 어떻게 되는지 보자.
hoyoul — 11/27/2023 8:28 PM 오타가 있었다. 다시 해보자. action은 어떻게 호출하는지 모르겠지만, socket연결하면 어떤 message를 보내는지 확인만하자.

Figure 86: gameroom5
socket이 null이 되었고, rails에서 bp를 걸면 더 좋을꺼 같다. [8:37 PM] flutter보다 rails에서도 bp를 걸려면 local에서 동작하게 하는게… [8:40 PM] { “command”:“subscribe”, “identifier”:”{\“channel\”:\“GameRoomChannel\”}” } [8:41 PM] 이게 일종의 protocol이다. socket에 실려서 주고받는 data의 payload의 형식. [8:41 PM] command는 rails에서 action cable 정의한곳에 보면 다음과 같은 게 있다. [8:42 PM] class GameRoomChannel < ApplicationCable::Channel rescue_from StandardError, with: :deliver_error_message
ONLY_GAME_ROOM_ID = “GameRoom-1”
def join puts “channel: join” @game_room = GameRoom.first stream_from(ONLY_GAME_ROOM_ID) end
def subscribed puts “channel: subscribed” stream_from(ONLY_GAME_ROOM_ID) end
def subscribe puts “channel: subscribe” stream_from(ONLY_GAME_ROOM_ID) end
def leave stop_stream_from(ONLY_GAME_ROOM_ID) @game_room = nil end [8:42 PM] 여기에 있는 join, subscribed, subscribe, leave가 command의 값이 되고, [8:43 PM] GameRoomChannel은 channel명이다. [8:43 PM] 이거 두개를 실어보내면, puts에 해당하는 값을 리턴 받을 듯하다. [8:44 PM] 내일 테스트하자. 좀 쉬고 회의하고 오늘은 일찍자야겠다.
11.28
const wsUrl = ‘ws://kholdem.fly.dev/websocket’; final webSocketService = WebSocketService(wsUrl); final message = { ‘command’: ‘subscribe’, ‘identifier’: ‘{“channel”:“GameRoomChannel”}’, }; String jsonString = jsonEncode(message); webSocketService.sendDataToServer(jsonString);
[9:29 AM] 다시 테스트
hoyoul — 11/28/2023 9:43 AM 제대로 안된다 [9:44 AM] rails가 제대로 받았는지 제대로 응답하는지 알수 없다. [9:44 AM] debugging을 해야 한다. [9:45 AM] local에서 할수 있게 해야 한다. [9:45 AM] 어차피 local에서 실행하고 연결하면 된다.
hoyoul — 11/28/2023 9:58 AM Received unrecognized command in “{“command”:“join”,“identifier”:”{\“channel\”:\“GameRoomChannel\”}"}" [10:01 AM] 아…actioncable은 사용법에 맞춰서 코딩해야 한다. [10:03 AM] 슈퍼좀 갔다오자.
hoyoul — 11/28/2023 10:14 AM subscribe하고 join을 하는데, 둘다 인식이 안되는데…
순상님한테 물어봐야 할듯하다.
hoyoul — 11/28/2023 10:37 AM json을 encoding하는건 내장 library사용하는데, 그냥 hash로 된 message에서 escape 빼고 보내라는거 같은데.. [10:39 AM] 어 됐다. [10:41 AM] 근데 이상한건 puts로 보낸게 안찍힌다.
hoyoul — 11/28/2023 10:47 AM stream처리하는 부분에 bp를 걸어봐야겠다. 실제 오는지.. [10:47 AM] 에러는 안나는데…좀 까봐야겠다. [10:54 AM] 아니다. subscribe를 인식 못한다. escape를 제거했는데도 인식 못한다.
hoyoul — 11/28/2023 10:56 AM 근데 내가 원래 escape빼고 줘서 문제 있는거 아닌가? [10:56 AM] 넣고 줘야하는거 아닌가 [10:56 AM] 테스트해보자.

Figure 88: gameroom7
puts 는 console출력이고, escape를 빼주면 안되는거였다.
hoyoul — 11/28/2023 11:03 AM socket stream이 따로 있기 때문에 puts는 console출력이다. [11:04 AM] 아까 하다만 join도 같이 보내보자. 어차피 될거긴 하다.

Figure 89: gameroom8
그냥 rails에서 받는것만 확인했다. [11:14 AM] 받을 때 문제가 있다. 원래 websocket은 tcp에 http붙인거라서, tcp의 특징 계속 연결을 확인한다. 이게 receive되기 때문에 filtering을 해주는데, flutter에선 함수가 있을꺼 같기도 한데… [11:16 AM] string에 contains라는 함수가 있다. 특정문자열 포함여부를 알려주는데, filter로 쓸수 있겠다.
hoyoul — 11/28/2023 11:21 AM 아직 join은 안되어 있기 때문에, intro page에서 방정보는 얻어오지 못한다. [11:24 AM] 근데 중요한건 설계인데, 설계가…내생각과 많이 다른데, 여튼 나는 그냥 맞춰서 처리하면 되니까…지금 해야할건, 게임화면을 만들다 만거하고, container로 모든걸 처리했던거를 수정하고, action같은것도 button으로 해야 ripple? 효과가 있으니까…전체적인 게임화면, 그리고 이동을 해야한다. [11:25 AM] 이동은 getX에서 쉽게 했으니, 문제는 없어보인다. obx처리가 문제인데… [11:26 AM] 그리고 card도 버튼으로 해야할지 container로 해야할지 생각해야 한다. [11:26 AM] 하나하나 해보자. [11:28 AM] 아..그리고 local로 처리했던거 운영서버?로 고치고 테스트도 해야 한다. 에러가 나는데, 아마 문제 없을꺼다.
hoyoul — 11/28/2023 11:30 AM 이왕 하는김에 emacs에서 rails설정도 하자. 하나하나 파일을 열어서 이동하고 definition도 일일이 찾아가는건 말이 안된다. grep도 rg로 변경하자. [11:30 AM] 운영서버 test [11:34 AM] 음…좀 이상하다. [11:35 AM] subscribe만 테스트하는데, local과 좀 다르다.
hoyoul — 11/28/2023 11:39 AM local에서는 subscribe를 하면 flutter에서 다음 메시지가 나온다. flutter: Received from server: {“identifier”:"{\“channel\”:\“GameRoomChannel\”}",“type”:“confirm_subscription”} [11:40 AM] 이걸 보고 subscribe가 제대로 됐다고 판단했다. [11:41 AM] 근데 내가 output화면이 많아서 이전 화면을 보고 착각했을수도 있다. [11:41 AM] emacs에 버퍼가 너무 많다. 다 지우고 운영서버 테스트해보자. [11:45 AM] 아…나왔다. [11:45 AM] flutter: Received from server: {“identifier”:"{\“channel\”:\“GameRoomChannel\”}",“type”:“confirm_subscription”} [11:45 AM] 에코 메시지가 나왔는데, 느리다. [11:46 AM] 많이 느리다. [11:46 AM] local과 비교하는거 자체가 말이 안되긴 하지만…
hoyoul — 11/28/2023 11:47 AM 시료도 해볼까? [11:48 AM] 아니다. 여튼 운영서버도 websocket이 된다는건 확인했다. [11:48 AM] 당장 고쳐야 하는게 container에서 button으로 바꾸는거 부터 하자. [11:49 AM] 그래야 게임화면 만들고 [11:49 AM] 애니메이션도 공부할 시간이 된다. [11:53 AM] git도 올려야 하나? [11:53 AM] git도 올리자. [11:54 AM] websocket 구현보다는 test만했다고 올리자.
hoyoul — 11/28/2023 11:58 AM container to button (edited) [12:02 PM] button을 한다는건 tap이나 press반응뿐 아니라, ui effect효과가 있다. 내가 단순하게 생각한건, container가 다루기 쉽고, 위치도 align하고 width height color, 자식들관계등…다 있기 때문에 만능 유틸리티 위젯처럼 생각했던게 있다. [12:03 PM] 그래서 gesture만 달면 button하고 똑같은데, 그냥 통일시켜… [12:04 PM] 이게 잘못됐다. 그러면 button으로 해야할건 login관련하고, action만 하면되는건가? [12:04 PM] 카드도 해야하는가? [12:04 PM] 확실한건 login하고 action이니까. 그부분을 먼저하고, card에 대해선 물어보자.
hoyoul — 11/28/2023 12:07 PM 10분만 쉬자. 눈아프다.
hoyoul — 11/28/2023 12:18 PM 새로운 button class를 만들어야 한다.
hoyoul — 11/28/2023 1:50 PM https://api.flutter.dev/flutter/material/IconButton-class.html
icon button이다. 좀 읽어보자. [1:54 PM] 특징적인건, 내부가 stateful로 되어 있다는 것이다. [1:55 PM] 그래서 setState를 쓴다. 이렇게 직접적인 stateful widget이 가능한가? [1:55 PM] icon 때문에 가능한가? [1:56 PM] 이건 아닌거 같은데… [1:56 PM] elevate button을 사용해야 하나? [1:56 PM] 내가 아는 button은 elevate와 floatbutton밖에 없는데…
hoyoul — 11/28/2023 1:59 PM https://api.flutter.dev/flutter/material/ElevatedButton-class.html
elevated button은 eventhandler가 없다면 disabled된다는게 특이하네…직관적이네.. [1:59 PM] 이것도 stateful [2:00 PM] icon에선 setState에서 변경할 data가 있었지만, elevate에선 상태데이터가 뭐길래? [2:00 PM] 예제에는 setState가 없음.
hoyoul — 11/28/2023 2:10 PM 별다른 건 없어보임.
hoyoul — 11/28/2023 2:20 PM elevate button을 넘겨버리면 될듯 [2:23 PM] 근데, size가.. [2:23 PM] 좀 이상해진다. [2:24 PM] container안에 넣어서 return하는게 맞나? [2:25 PM] minimumSize밖에 없다. 이렇게 되면 문제가 된다. [2:25 PM] container로 감싸는게 맞다.
hoyoul — 11/28/2023 2:46 PM 엥, elevatebutton은 image를 넣을수 없다. 그러면 row로 해서 image, text해야 하나? [2:47 PM] 아..그리고 image의 폴더도 바꿔야 한다. image가 lib에 있는건 이상하다. [2:47 PM] assets폴더아래에 있어야 한다.

Figure 90: login1

Figure 91: login1-2
11.29
action button도 button으로 처리
hoyoul — 11/29/2023 12:02 PM 안쓰는 ui와 잘못 설정된 UI는 정리가 끝났다. [12:03 PM] login버튼 눌렀을때 코드를 지금은 삭제했는데, 다시 코드 심고 git에 반영한후 game page만들고, [12:03 PM] 게임은 통신하면서 처리하면될 듯 하다.
hoyoul — 11/29/2023 1:23 PM 난 코드에 관대하다고 생각했다. 개발자들은 남의 코드를 평가하면서 개판으로 짠 코드네 뭐라한다. 나는 그런건 뻘소리라고 귀기울필요가 없다고 생각했다. 왜냐면 원래 개발은 매번 수정을 하기 때문에 개판되는건 당연한 일이기 때문이다. 개판이 안되는게 더 이상하다. 개판은 문제가 아니다. 하지만, 짜증나는건, 주석이라던지, 문서라던지, 한마디 말이라도 있어야 하는데, 없을때 짜증난다. 상상해야 하기때문이다. 자연님이 섞어쓰던, 갈아쓰던 그건 고민의 흔적이다. 하지만, 보는 사람에게 일언반구 하나 없었기 때문에 짜증난거고, 설계도 마찬가지다. 설계에 대한 평가는 평가하는 사람이 하는거고, 돌아가기만 하면된다. 그런데 어떤식의 생각인지 문서나, 설명이 없다면 상상해야 한다. 그렇게 되면 짜증난다.
hoyoul — 11/29/2023 1:32 PM login code를 삽입하는건 끝냈다. web socket test한것도 넣어놓자. 그리고 git에 올리자.
hoyoul — 11/29/2023 1:45 PM websocket테스트하니 지금은 unauthorized로 연결은 안되는데, 어제 test한거라서 git에 올리자. [1:45 PM] 아…card test를 안했다. card가 나오는지만 확인하고 올리자.

Figure 92: ui1
각각의 폴더는 대부분의 ui요소들을 가리킨다. 하나라도 중복이 되는것을 기준으로 했고, 만일 추가되거나 변경되면 해당 폴더에서 처리 하면된다. 사용법은 최대한 간단하게 해서 그냥 가져다가 조립하면 된다. 이제 이것을 바탕으로 game page를 만들면 된다. [2:07 PM] game page를 만들면, socket통신은 예제가 있으니까, 값만 변경하면 된다. [2:08 PM] 이게 getX를 사용하기 때문에 매우 간단해 진다. [2:08 PM] 즉 통신된 값을 obs로 설정해주고, 변경되는 값은 obx로 해주면, 가져온 값이 다르면 update가 자동으로 일어나는 것이다. [2:10 PM] game page작성
hoyoul — 11/29/2023 2:18 PM import ‘package:flutter/material.dart’;
class GamePlayPage extends StatelessWidget { const GamePlayPage({super.key});
@override Widget build(BuildContext context) { return const Scaffold( body: SafeArea( child: ) ); } } [2:19 PM] 기본 page에 로고블럭처럼 기술하면된다. [2:20 PM] 레이아웃은 크게 top과 bottom으로 나누자. [2:22 PM] 그다음 component를 넣어 넣고 page 자체를 snippets으로 만들어 놓으면 그냥 붙여넣기 해서 사용하면 된다. 즉 다음에 페이지만들일이 있으면 그냥 1초면 만들어지는것이다. 그래서 원래 제대로 된거 한번만 만들면된다. 그리고 그 다음부터는 snippet에서 placeholder만 만들면 1초에 page하나씩 만들수 있는것이다. [2:23 PM] 그래서 처음 작업만 잘하면 된다. [2:24 PM] 나는 처음 작업한것을 사용할 수 없는 상태다. 왜냐면 기본이 되는 component를 다시 바꿨기 때문이다. 그런데 game page를 만드는것도 2-30분이면 되고, snippet을 만든 후부터는 5분내에 template page만들수 있다.
hoyoul — 11/29/2023 2:26 PM 4개로 나누자. gameHud, gamePlace, myPlace, myActions로 만들자. [2:26 PM] 그리고 row를 사용하면 될듯하다. [2:30 PM] gamehud붙여넣자. [2:33 PM] gamehud에 $, pot, 1000, $,call, 1000,exit, profile 8개의 요소가 있다.
hoyoul — 11/29/2023 2:33 PM 8개의 요소를 layout하지 않고 그대로 row에 일렬로 배치한다. [2:34 PM] 즉 row만 잡아놓고 다 일렬로 배치하면 된다. [2:34 PM] 슈퍼좀 갔다오자.
hoyoul — 11/29/2023 2:46 PM 이제 하나씩 하면 된다. [2:47 PM] $는 PlayerInfoButtonComponent(infoType: InfoType.dollor), [2:47 PM] 이렇게 표시하면된다. [2:48 PM] pot는 TextFactory.createText(‘pow’, TextType.playerSeedText), [2:51 PM] 어..에러가 있네. [2:53 PM] 오타가 있었다. [2:53 PM] 여튼 이제 동일한 형태니까. 복불하자. [2:53 PM] 테스트 해보자.

Figure 93: image1
$ 이미지가 이상하다. 에러만 고치고 20정도 공간주면된다. [2:58 PM] dollar인데 dollor로 했다. [3:02 PM] 음..변경했는데도 에러다.

Figure 94: image2
emacs를 다시 껐다 켰다. [3:05 PM] 두번째가 gameplace다. [3:06 PM] 4명의 사용자가 있다. column에 row가 있는 식이다. [3:06 PM] 1명의 사용자만 만들면 재사용하면 된다. [3:07 PM] 이것도 component로 만들었어야 하는데, 지금은 이건 안했다. [3:07 PM] 우선 한 사용자만 만들자. [3:07 PM] 화투패, 화투패, betting정보가 있다.

Figure 95: image3
문제가 있다. [3:23 PM] 내가 card를 테스트를 보여주는것만 했는데.. [3:23 PM] card가 type이 있다. 5짜리와 10짜리를 분리했놨는데 [3:23 PM] 지금 10짜리로 출력했는데 5짜리가 보인다. [3:24 PM] 뒷면도 테스트를 못했다. [3:24 PM] 뒷면은 색이변한거 같아서, 이전이미지와 다른데…여튼 [3:24 PM] 그냥 이렇게 놓자. [3:25 PM] 이왕하는김에 이거 component로 만들고 [3:25 PM] type만 고치자. [3:26 PM] 10분간 쉬자. 눈아프다.
hoyoul — 11/29/2023 3:42 PM type은 고쳤고 [3:42 PM] component로 만드는건 우선 보류하자.

Figure 96: image4
했는데, component를 안만들면 obx를 여러개 해야되는데…이렇게 하면 안된다. [3:51 PM] 게임을 테스트 하는데 더 오래걸린다. [3:52 PM] component를 만들어야지 변수로 접근해서 [3:52 PM] 처리가 가능하지 static하게 copy &paste로 처리할수 없다.
hoyoul — 11/29/2023 4:21 PM seatComponent를 만들었다.

Figure 97: image5
최대한 중복을 줄였는데도 코드양이 많다.

Figure 98: image6
크기 조절이 안된다. [5:00 PM] 이것은 card에서 test를 안했다. 그리고 container의 배경색 바꾸는건만 하고 action list출력하자.
hoyoul — 11/29/2023 7:32 PM 아니다. 먼저 action list하고, 크기조절 배경색 바꾸자.

Figure 99: image7
약간 정렬하고 background color 그런거 변경시키고 그럴듯하게 만들고, 게임 시작하면 animation으로 화투패 한장씩 돌릴수도 있고. [7:48 PM] animation이야 어차피, scaffold의 canvas를 paint repaint하지 않을까? flutter라면 더 쉬우면 더 쉽지 어렵진 않을듯…그냥 하라는대로 적힌대로 하면 될테니까.. [7:49 PM] widget은 통째로 그릴테니 신경안써도 되고, transition을 하던 rotate같은건 그냥 함수로 주어질테고.. [7:50 PM] 여튼 action버튼좀 정리하고

Figure 100: image8
원래 크기대로 하면 글자가 내려간다. 글자 크기에 문제 있는지 확인은 안했는데, 그냥 버튼크기를 늘렸다. [8:00 PM] player seat 배경색을 검은색으로 하고 카드크기를 player와 opponent가 다르게 해야한다. [8:01 PM] 아…빨래 가지러 가야겠다.
hoyoul — 11/29/2023 8:05 PM 배경색을 바꿔보자.

Figure 101: image9
카드 사이즈만 좀 확인하자. 원래 card size는 더 작게 나와야 하는게 정상이다.

Figure 102: image10
copy & paste해서 card type이 player 카드타입으로 되어 있어서 크게 나왔다. cardtype을 바꿨다. [8:21 PM] animation을 할려면 예제를 봐야 한다. [8:22 PM] 화투게임이나 포커게임에서 보통 어떤식으로 애니메이션이 되어 있는지 보고 거기에 사용된 효과를 찾아서 적용하면 된다. [8:22 PM] 피망을 깔아보자. [8:23 PM] 난 섯다를 모른다. 9시까지 피망하면서 룰도 익히고 애니메이션도 보자. 어떻게 되어 있는지.. [8:23 PM] git에 반영만 하자. 이건 미완성이다. 그냥 복사 붙여넣기도 많이 사용해서 코드도 복잡하다. [8:26 PM] 반영했다.
hoyoul — 11/29/2023 8:28 PM 나이제한이 걸려있다. 그런데, 클릭이 안된다. 뭥미..

Figure 103: pmang1
얘네들은 창을 만들어서 consent까지 버튼으로 구현했다. third party가 사용자 resource도 접근하게 했다. [8:34 PM] 근데 뭔가 잘못입력하면 어디서 잘못됐는지 표시되야 하는데, 얘네들은 그게 없다. 입력이 올바르지 않다고만 상단에 표시할 뿐이다.

Figure 104: pmang2
인증코드를 보내주는데, 이건 어떻게 하는건지 궁금하다. [8:38 PM] form을 작성해서 verify를 한다음 통과되면 다시 인증코드 보내주고…근데 웃긴건..인즈코드 통과됐는데, 자동으로 다운로드가 안된다. [8:38 PM] 다운로드를 다시 해야 한다.ㅋ [8:39 PM] 게임을 시작하니, consent screen 뜨네…이건 우리하고 똑같다. [8:40 PM] 아…다르다. [8:40 PM] 두번뜬다. [8:40 PM] 이건,,,첫번째 뜨는건 resource에 대한것이고, 두번째가 consent다. [8:42 PM] 또 본인 인증함. [8:43 PM] 근데 landscape모드가 디폴트인가?
hoyoul — 11/29/2023 8:44 PM 그리고 게임은 전체화면을 사용한다. [8:44 PM] 방장이 강퇴했다. 어떻게 하는지 하나도 모르겠다. 어렵다.
hoyoul — 11/29/2023 9:26 PM 우선 화투패를 건네주는건 공중에서 뿌려지는 방식인데, 순식간이다. 작은 화투패가 원래 크기로 되는건데, 이건 scale효과다. [9:26 PM] 처음에는 작은 size였다가 크게 되니까, scale은 주고, translate로 중앙에서 원래 자리로 translate도 있는듯하다. [9:27 PM] 이건 flutter에서 scale과 translate효과를 찾아서 예제보고, 카드를 demo로 하면 비스무리하게 될꺼같긴 하다.
11.30
getX로 game화면 진입을 처리하자.
hoyoul — 11/30/2023 8:46 AM 사용자 token:
eyJhbGciOiJIUzI1NiJ9.eyJlbWFpbCI6ImVuZ2luZWVyZWQuZGVzaWduQGdtYWlsLmNvbSIsImV4cCI6MTczMjc4NjE5OH0.WVi4yn3PZJLhHd5i_-fgfsvmaY6o—deILl5ZGFwac
header auth에 설정, postman 에서 웹소켓 2번째에도 적용.
hoyoul — 11/30/2023 8:53 AM 이게 login했을때 받은 idToken아닌가…레일즈에 3개 token주고 받은 token인지는 bp걸어서 확인하고.. [8:58 AM] emacs에서 ios simulator만 사용해서 확인하는데, android emulator도 확인할 수 있게 key binding한다. [8:59 AM] 두개의 emul, simul을 사용하고 시료도 해서 3개로 동시에 접속 여부도 알수 있다.
hoyoul — 11/30/2023 9:05 AM android emul [9:06 AM] android를 터미널에서 실행하는것은 emulator -list-avds [9:06 AM] 이걸로 어떤 avd가 있는지 확인후 실행해야 한다. [9:07 AM] 난 Pixel_6_API_34을 사용하기로 한다. [9:10 AM] qt를 못찾아서 에러가 나면 echo $ANDROID_SDK_ROOT [9:10 AM] path가 설정되었는지 확인하자.
hoyoul — 11/30/2023 9:12 AM 아니면 android studio의 update나 동작이 되는지 확인하자. 내경우 update후 동작이 안된다. [9:12 AM] rebooting하자.
hoyoul — 11/30/2023 9:20 AM 내경우 path에 없기 때문에 .zshrc에 다음을 추가하자. export ANDROID_SDK_ROOT=$HOME/Library/Android/sdk export PATH=$PATH:$ANDROID_SDK_ROOT/emulator export PATH=$PATH:$ANDROID_SDK_ROOT/tools export PATH=$PATH:$ANDROID_SDK_ROOT/tools/bin export PATH=$PATH:$ANDROID_SDK_ROOT/platform-tools [9:22 AM] source해주고 emulator -avd Pixel_6_API_34 [9:22 AM] 그런데 에러가 나네.ㅠ
hoyoul — 11/30/2023 9:32 AM https://stackoverflow.com/questions/71015608/how-can-i-launch-android-emulator-without-android-studio-on-mac-m1 m1이라서 나는 문제다.
hoyoul — 11/30/2023 9:42 AM 이렇게 해도 안된다.
hoyoul — 11/30/2023 9:58 AM 이게 되면 좋은데…android studio에선 문제없이 된다.

Figure 105: emulator1
되긴 했다. 근데…emacs가 먹통되는 문제가 있다. [10:13 AM] emacs자체에 bp를 걸었는데, thread가 잡고 안놔준다. 이런경우 새롭게 만들던지, update를 기다려야 한다.
hoyoul — 11/30/2023 10:19 AM getX 로 gameplay로 이동 (edited) [10:19 AM] 방정보를 얻은 후에 game화면으로 이동해야 한다. [10:22 AM] 방정보를 얻기위해서 token을 사용하라고 줬는데, 이것을 복사해서 사용하는건 말이 안되고, 왜냐면, 처음에 google에서 받은 3개의 토큰을 rails에 준건, exp와 token을 받기 위해서이다. 여기서 받은 token이 rails와 인증목적인데, 그래서 받은건데…다시 토큰을 하드코딩하는건 아니다. [10:23 AM] 그러면 받은 토큰이 동일한 토큰인지 확인하자. 그리고 http로 받을 때는 post로 데이터를 주고 받지만 socket은 json으로 받는다. 좀다르다. [10:24 AM] http의 get은 url로 데이터로 보내던가 post로 해서 bearer로 보내고 했지만, web socket은 주고 받는 data형식이 있다. 그것을 json형태의 protocol을 사용하기 때문에 json에 넣어서 보내는게 맞는데, header auth에 설정? [10:25 AM] postman을 보자.
hoyoul — 11/30/2023 10:27 AM 왜 이렇게하는지 모르겠다. 왜냐면, rest api같은 경우는 get방식을 쓰면 header에 token을 넣는다. 왜냐? url로 넣으면 그냥 평문이고, 그냥 해킹 당한다. [10:27 AM] 그래서 header에 넣는데, 암호화해서 넣는다. [10:28 AM] post방식도 마찬가지다. 그냥 평문으로 넣으면 안된다. 그리고 그냥 body나 url에 넣지 않는다. header에 넣는다. [10:28 AM] 근데 websocket은 url이나 html body로 전송하는게 아니다. [10:29 AM] tcp의 payload에 실어서 보낸다. [10:30 AM] 물론 web socket은 web+socket이다. 그래서 tcp위에 http로 감싼형태인데 [10:31 AM] 그 http header에 넣는다는거다. [10:31 AM] 동작은 문제 없을꺼다. [10:31 AM] 그런데 좀 이상한거지…여튼 해보자.

Figure 106: emulator2
login할때 받은 token과 비교하기 위해서 bp를 걸고 확인해보자.
hoyoul — 11/30/2023 10:40 AM 그런데 postman에서 host와 scheme이라는 변수비슷한 환경세팅이 가능한지 찾아보자. 저렇게 썻다는건 정의해서 쓴다는 느낌이다. [10:40 AM] http://developer.gaeasoft.co.kr/development-guide/knowledge/postman/how-to-use-variables-in-postman/ [10:42 AM] environment가 옆에 있다. kholdem-dev는 local [10:42 AM] kholdme-prod는 production이다.
hoyoul — 11/30/2023 10:59 AM postman에서 test는 subscrib한후 join하면 되나? 원래 subscribe를 하지 않고 join을 하면 안될꺼다. [11:00 AM] subscribe가 없으면 안되는걸로 알고 있다. [11:01 AM] 그냥 코드로 하자. 왜냐면 postman에서 디버깅을 걸수 있는것도 아닌데, 여기서 filtering작업도 하고 뭐도 세팅하고 불편하다. [11:02 AM] 코드에서는 이미 subscribe후에 join이 되는걸 확인했으니까 [11:02 AM] 바로 join은 안되는것도 확인했고… [11:05 AM] web socket을 사용하는데 왜 header에 token을 넣어놨는지 알겠다. postman에 보면 restapi하고 websocket하고 비슷하게 생겼고, http쓰니까. 그렇게 한거 같다. [11:06 AM] 여튼 그냥하자.
hoyoul — 11/30/2023 11:09 AM 근데 에러처리를 어떻게 할려고 하지? [11:09 AM] 똑같은 http니까 똑같이 http response error를 사용하려고 하나? [11:12 AM] rest api와 socket은 차이가 있는데, 어차피 구현은 그냥 header넣으면 된다. bearer를 사용해서 넣으면 된다. 하자. [11:14 AM] 근데, 이게 절차지향으로 짜서…처리가.. [11:15 AM] google auth token받아온다. [11:15 AM] rails에게 token주고 token받아온다. [11:15 AM] websocket연결한다. [11:15 AM] subscribe한다. [11:16 AM] join할때 token넣어서 보낸다.
hoyoul — 11/30/2023 11:16 AM 이렇게 하면 게임이 되나? 이렇게 하면 다 static으로 처리하거나… [11:18 AM] rest api로 token을 가져온것을 변수로 처리해야한다. [11:18 AM] static변수로, 그리고 socket에서 보낼때 해당 변수를 참조해서 보내는건데.. [11:18 AM] 이렇게 짜는경우는 처음본다. [11:22 AM] 자연씨가 이렇게 짰는데, 자연씨는 여기서 getX까지 static으로 처리해서 완전 스파게티가 됐는데..
hoyoul — 11/30/2023 11:23 AM stream을 쓰면 될까? [11:23 AM] observer pattern을 써서…해야 하나… [11:25 AM] 인증에 관한 객체하나를 유지할 필요는 있다. [11:26 AM] 그리고 그 객체값을 이용하는 식으로 해야 한다. 이렇게 하면 서버와는 별개로 노는거긴 한데… [11:26 AM] 첫단추가 잘못되면 계속 잘못된다. [11:26 AM] user모델을 만들지도 않았고, [11:27 AM] user가 없으니, 인증모델은 또 따로 놀고.
hoyoul — 11/30/2023 11:34 AM 우선 rest api로 인증을 맺으면 인증객체가 만들어지면서 살아있어야 한다. [11:34 AM] 그리고 socket을 보낼때 인증객체에 있는 값을 꺼내서 사용한다. [11:34 AM] 인증객체에는 expired time이 있어서, 시간이 종료되면 notification을 날리는 식으로 만들어야 한다. [11:36 AM] 서버에서 객체를 주고 받는 식이 아니라, client에서 재조합해서 만드는 식으로 해야 한다.그러면 client객체는 늘 부분완성이다. 한번에 생성하지 못하고, 그냥 c의 structure라고 생각하고 처리해야 한다. [11:37 AM] 객체를 만드는게 아니라 매번 structure를 만들어서 사용하는 식으로 해야 한다. [11:37 AM] 절차지향으로 짜는건데…이것밖에 떠오르는게 없다. [11:38 AM] 다른 방법이 있을까? [11:39 AM] 여튼 방법이 나왔으니 이거대로 구현을 해보자. [11:39 AM] AuthThing이란걸 만들자. [11:40 AM] 만들기 전에 display name을 return해달라고 요청해야 할듯 하다.
hoyoul — 11/30/2023 11:51 AM 우선 safety null을 사용해서 멤버변수를 처리하더라도 [11:52 AM] 주어진것은 email, exp, token, name(display)이건데 이 object의 목적은 하나다. [11:52 AM] 아, 두개다. [11:52 AM] 하나는 exp유지하는 목적이고, [11:52 AM] 다른 하나는 정보를 사용하는것이다. [11:54 AM] 어차피 user들은 인증이 성공하면 exp를 갖는 object를 갖게 되고, 이것은 대략 1년인데, 로긴할때 마다 shared preference에서 꺼내서 check할 것이다. [11:55 AM] 그러면 class이름도 AuthThing이 아니라, Exp관련한걸로 이름을 지어야 하지 않을까? [11:56 AM] 여기서 User의 member로 exp클래스를 가지고 있는게 좋은데 [11:57 AM] user를 만들수가 없다. 왜냐 정보가 한번에 넘어오지 않고 겹치기 때문에…이렇게 하면 안되고…우선 시나리오는 그럴듯하다.
hoyoul — 11/30/2023 12:00 PM TokenInfo라고 생각했는데, TokenExpiration이란 class가 더 나을듯하다. [12:00 PM] 왠지 expiration에 focus를 둔거 같다. [12:01 PM] (1) TokenExpiration class를 만든다. [12:01 PM] (2) login이 처음이라면 shared에 저장할때 객체생성 [12:02 PM] (3) login이 처음이 아니라면 shared에서 꺼낸정보로 객체 생성 [12:02 PM] (4) websocket에서 stream을 사용하던, observer를 사용하던 getx의 controller를 사용하던 TokenExpiration객체를 가져와서 token을 bearer로 해서 보낸다. [12:03 PM] 이 과정에서 발생되는 에러처리도 고민한다. [12:05 PM] 이거 하나만 되면 나머지 통신들도 어차피 동일한 패턴으로 가져와야 하니까. general한 함수하나 만들고, enum으로 api에 해당하는 함수명을 나열해서 general한 함수에 인자로 enum의 값을 사용할 수 있게 하자. general한 함수에는 switch를 달아서 enum값에 따라서 socket통신을 하게 만들어야 한다. 이렇게 되면 [12:05 PM] 통신부분은 마무리되고, 통신으로 가져온 값들은 위에서 TokenExpiration처럼 객체로 유지하면서 상태데이터에 obs를 붙여놓는다. [12:06 PM] view에서 obx로 상태데이터 처리하자. 이렇게 하면 게임이 진행 되지 않을까? 위에서 (4)번 이후는 다시 재작성해서 번호를 붙이고 구현하자. (edited)
hoyoul — 11/30/2023 12:07 PM (1) Token Expiration class를 만들자. [12:11 PM] class TokenExpiration { String name, String email, String authToken, Date expiredTime, TokenExpiration() {} } [12:12 PM] 대충 이런식으로 만들건데, exp는 unix time으로 int형으로 되어 있다. 이것을 그냥 int형으로 유지하고, 보여줄 때만, 함수하나 만들어서 변환처리를 해도된다. [12:13 PM] 그냥 int가 나아보인다. 원래 받은값은 그대로 유지하고 사용할때 변환하는게 더 좋아 보인다. 변환해서 Date로 저장하는것보다 더 낫다. 바꾸자.
hoyoul — 11/30/2023 12:28 PM 대충만들었다. 음…그리고 project의 설계에 대해서 내가 아는것을 잊어버리지 않도록 적어보자. 솔직히 정형화된 방식이 있다고 보지 않지만, sw공학이란 과목에서 배우고, 또 실전에서 이렇게 사용하기 때문에 적어본다. 내가 개발하거나 내가 만들어낸게 아니라, 그냥 그렇게 배웠고 그렇게 사용하기 때문에 나만의 고유한 지식도 아니고, 맞다고 볼수도 없다. lg에서 소규모 팀단위로 움직였는데, 그때 prototype을 만들었다. prototype은 mpv보다 작은 그냥 기능동작만 하는 간단한 program이다. [12:29 PM] 여기서 사용된 방식을 제주에서도 사용했었다. [12:32 PM]
- 사전 조사를 한다. 내경우 태양광시스템을 만들었는데, 처음하는건 사전조사다. 즉 태양광시스템을 구성하는 발전소, 수배전반이라는 큰 두개의 class가 있고 발전소에는 각각의 장비들이 별도로 있다. 수배전반도 그렇다. 발전소나 수배전반의 속성들을 다 적는다.그러면 class가 한 10개 나온다. 이것들을 모두 UML로 정리한다. 각각의 class와의 관계, sequence 필요한걸 다 문서화 한다. (edited)
[12:32 PM] member변수만 모아서 만든다. [12:33 PM]
- 이것을 구현한다. rails로 사용할때는 그것들이 다 model이다.
[12:33 PM] model을 다 만들면 관리자 gem사용해서 관리자 page도 만들수 있다. [12:33 PM]
- TDD를 한다.
hoyoul — 11/30/2023 12:36 PM client에서 해당 모델을 접근할 수 있는지 test만 한다.그리고 client에 똑같은 형태로 객체형태로 처리한다. rails에선 별도의 view가 있기 때문에 그대로 사용하면 된다. frontend가 떨어진게 아니다. [12:37 PM] 이렇게 하면 client와 server의 큰 기능동작이 완료된다. 즉 정보를 가져와서 보여주고 하는게 다 되는것이다. [12:38 PM]
- 이 이후에는 그냥 기능구현이다. 이것을 tdd로 해도되고 어떻게 하던 상관없다. 그냥 요구사항에 맞춰 짜는것이다.
[12:38 PM] 아..밥먹고 오자.
hoyoul — 11/30/2023 1:39 PM class를 만들었다. 이것을 userController에서 처리할 것이다. 여기서 처리하는 이유는 userController는 business logic을 처리함과 동시에 view에 반영할수 있는 GetXController를 상속받은 class이기 때문이다.
hoyoul — 11/30/2023 2:29 PM 고치다 보니 꼬였다. [2:29 PM] 적으면서 해야겠다. [2:30 PM] login했을때 preference가 있는 경우 (edited) [2:30 PM] Future<void> checkLogin() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); // await prefs.clear(); bool hasData = prefs.containsKey(’email’) && prefs.containsKey(‘idToken’) && prefs.containsKey(’exp’);
if (hasData) {
tokenExpiration = TokenExpiration(
displayName: await fetchDataFromSharedPreferences('name'),
email: await fetchDataFromSharedPreferences('email'),
authToken: await fetchDataFromSharedPreferences('authToken'),
expTime: await fetchDataFromSharedPreferences('exp'));
//gamelobbypage로 이동. gamelobbypage = gameintropage
Get.to(const GameIntroPage());
[2:31 PM] shared preference를 가지고 왔을때 data가 있다면 이전에 login했던것이다. [2:31 PM] 그래서 tokenExpiration객체를 preference로부터 가져와서 생성한다. [2:31 PM] 그리고 gameIntroPage로 이동한다. [2:32 PM] 여기까지는 된거 같다. [2:32 PM] login 했을때, preference가 없는경우 [2:32 PM] 처음 login한것이다. [2:32 PM] 그러면 인증과정을 거쳐야 한다. [2:33 PM] await executeOauth(); [2:33 PM] 이함수를 호출하는데, 이게 인증처리한다. [2:33 PM] Future<void> executeOauth() async { OauthService oauthService = OauthService(); dio.Response response = await oauthService.relayGoogleToken(); extractDataFromResponse(response.data); insertDataIntoSharedPreferences(); } [2:34 PM] google하고 통신하고, google로 부터 얻은 토큰을 rail로 뿌리고 얻는게 relayGoogleToken()다. [2:34 PM] rails로 부터 response객체를 받아서, data로 뽑는다. [2:35 PM] 뽑아서 controller에 member변수를 세팅하는 부분인데 이 부분을 바꿔야 한다.
hoyoul — 11/30/2023 2:36 PM 여기서 TokenExpiration객체를 세팅하자. [2:36 PM] 그리고 preference에 저장한 후에 gameIntroPage로 나가면 된다. [2:41 PM] 여기서 naming이 이상하다. 통일할 필요가 있다. [2:42 PM] name, email, token, exp로 통일하자. 왜냐면 displayName, authToken, idToken, expTime이렇게 다 다르면 안된다.
hoyoul — 11/30/2023 2:49 PM Future<void> insertDataIntoSharedPreferences() async { final SharedPreferences prefs = await SharedPreferences.getInstance(); tokenExpiration = TokenExpiration( name: await fetchDataFromSharedPreferences(’name’), email: await fetchDataFromSharedPreferences(’email’), token: await fetchDataFromSharedPreferences(’token’), exp: await fetchDataFromSharedPreferences(’exp’));
if (name != null) { await prefs.setString(’name’, name!); } if (email != null) { await prefs.setString(’email’, email!); } if (token != null) { await prefs.setString(‘idToken’, token!); }
if (exp != null) {
await prefs.setString('exp', exp!);
}
} [2:51 PM] preference에 저장할때 null check를 한다. 근데 내가만든 객체는 required parameter를 사용하는데, 이렇게 하면 객체를 만들수 없다. 객체의 생성자의 parameter를 required하면 안되고, null safety처리하고 setter를 만들어야 한다. 그래야 하나하나 집어넣을 수 있다. (edited) [2:55 PM] class TokenExpiration { String? name; String? email; String? token; int? exp;
TokenExpiration({ this.name, this.email, this.token, this.exp, });
// Setter void setName(String newName) { name = newName; }
void setEmail(String newEmail) { email = newEmail; }
void setToken(String newToken) { token = newToken; }
void setExp(int newExp) { exp = newExp; } // expired time convert to human friendly time. String getDatefromExp(int exp) { DateTime expiryTime = DateTime.fromMillisecondsSinceEpoch(exp * 1000); String formattedExpiryTime = ‘\({expiryTime.year}-\){expiryTime.month}-${expiryTime.day} \({expiryTime.hour}:\){expiryTime.minute}:${expiryTime.second}’;
return formattedExpiryTime;
} } [2:56 PM] 이렇게 바꿨다.
hoyoul — 11/30/2023 2:56 PM null safety처리하고 setter처리했다. [2:56 PM] 이제 insertData…를 변경하자. [2:58 PM] getter도 만들자. [2:59 PM] 근데 setter와 getter를 간단하게 나타내는 방법이 있을텐데…
hoyoul — 11/30/2023 3:08 PM 처리가 끝나긴 했는데… [3:09 PM] 그러면 websocket에서 여기서 설정한 token을 넣어서 보내는거를 처리해야 한다. [3:11 PM] UserController에서 생성한 객체를 webSocket에서 사용할려면, userController에서 websocket을 생성하고, 인자로 tokenexpiration을 전달해야 사용할 수 있다. [3:13 PM] 원래 page마다 controller,model,view로 설계했는데…그리고 getX를 사용해서 하는게…조금 이상할 수 있다. [3:14 PM] 5분만 쉬자
hoyoul — 11/30/2023 3:22 PM 그런데 UserController에서 webSocket연결을 하게되면 game에 관한것도 여기서 다 처리해야한다. 왜냐면, socket connection을 연결하고 stream이 userController에서 있게 되기 때문에 받는 메시지 처리를 여기서 다하는 형태가 된다. [3:25 PM] 아니면… [3:26 PM] 음…
hoyoul — 11/30/2023 3:32 PM 여튼 UserController에서 socket을 만들고 여기서 통신하면서 data를 처리하면 안된다. [3:33 PM] 차라리 SocketService에서 game에 관한 전반적인 처리를 하는게 오히려 나을수도 있다. [3:35 PM] UserController에 인증 토큰인 TokenExpiration객체가 있으니까, UserController에서 SocketService를 만드는 작업을 한 후에, 인자로 TokenExpiration을 전달하는 방법인데… (edited) [3:36 PM] 이렇게 하는게 그나마 괜찮다. UserController에서 다하면 꼬인다. (edited)
hoyoul — 11/30/2023 3:39 PM 내가 원하는 그림은 이거다. [3:40 PM] 우리 앱은 3개 page에요. login page, game room page, gameplay page로 되어 있어요. [3:41 PM] 각각 model이 있고요, 각각 controller가 있어요. 그래서 각각의 모델은 rails와 통신해서 필요한 정보를 가져오면 바로 반영될 수 있도록 getX를 사용하고 있어요. 간단하죠? [3:41 PM] 뭐 이렇게 할려고 했는데…. [3:43 PM] 통신은 각각 model별로 channel을 사용하는데, login channel, gameroom channel, gameplay channel이 있어요. 통신은 websocket사용하고 json document를 주고 받아요.
hoyoul — 11/30/2023 3:56 PM 그럼 userController를 loginController로 바꾸고 loginController를 userController에서 생성하고 인자로 TokenExpiration객체를 전달하면 되는데, 이것들이 다 getX의 controller라서… [3:57 PM] getX에서 controller관리를 어떤식으로 했는지 기억이 안난다. [3:58 PM] 그냥 생성만 하면 등록되는데…그럼 userController에서 등록하는 방식이 된다.
hoyoul — 11/30/2023 4:06 PM 그럼, GameRoomsPage로 이동하기 전에, GameRoomsController를 생성하고 Gex에 등록한후에 이동하는 방향으로 하자. [4:07 PM] / 처음 login이 아니라면 tokenExpiration만 만들고 gameRooms page로 이동 if (hasData) { tokenExpiration = TokenExpiration( name: await fetchDataFromSharedPreferences(’name’), email: await fetchDataFromSharedPreferences(’email’), token: await fetchDataFromSharedPreferences(’token’), exp: await fetchDataFromSharedPreferences(’exp’)); } else { await executeOauth(); / Get.to(const GameRoomsPage()); } //gameRoomspage로 이동 Get.to(const GameRoomsPage()); [4:08 PM] 여기서 gameRooms page로 이동하기 전에 GameRoomsController를 만드는데 이때 tokenExpiration객체를 인자로 전달하던가, 아니면 tokeExpiration에 obs를 걸어두면, observer로 사용되서 gameroomsController에서 가져다가 사용할수도 있다.
hoyoul — 11/30/2023 4:32 PM Get.put(GameRoomsController(tokenExpiration)); [4:33 PM] 이렇게 등록했고, [4:33 PM] class GameRoomsController extends GetxController { final TokenExpiration tokenExpiration;
GameRoomsController(this.tokenExpiration); //GameLoginController부터 받아온다.
void launchWebSocket() { * web socket test * const wsUrl = ‘ws://kholdem.fly.dev/websocket’; / server test / const wsUrl = ‘ws://localhost:3000/websocket’; // local test final webSocketService = WebSocketService(wsUrl);
webSocketService.checkWebSocket();
webSocketService.channel.stream.listen((message) { print(‘Received from server: $message’); }); }
@override void onInit() { super.onInit(); launchWebSocket(); } } [4:33 PM] GameRoomController를 만들었다. [4:34 PM] 여기서는 인자로 token을 받고 websocket을 초기화한다. [4:34 PM] 생성자에서 초기화를 안하고, onInit에서 했다. [4:34 PM] 차이는 없는데, getX에서 oninit은 등록할때 한번만 실행된다. [4:35 PM] 생성자에 하면 생성될때마다 실행된다. [4:35 PM] 그 차이밖에 없다. 근데 어차피 한번만 생성해서 계속 사용할 생각이라서… [4:36 PM] 여기서 두가지를 구현해야 한다. [4:36 PM]
- token객체에서 token을 bearer에 넣어 보내는 부분
[4:37 PM]
- webSocketService에서 subscribe와 join 을 함수로 처리하고 controller에서 호출하게 하자.
[4:37 PM] 1)의 bearer는 join호출시 인자로 전달하면 된다. [4:38 PM] 근데 restapi를 사용하지 않고 websocket을 사용했다고 하면 subscribe에서 토큰을 받아서 join에 사용했을것이다. [4:39 PM] 지금 subscribe에도 token없으면 접속 안되게 했을꺼 같은데… December 1, 2023
hoyoul — 12/01/2023 12:31 AM 우선 구현은 다했고, test도 해봤는데, 안된다. [12:31 AM] 우선 서버에서 name을 response하는 것을 확인했는데,
</img/oauth/websocket2.mp4>
name은 못가져오는거 같고.. [12:32 AM] token을 붙여서 websocket연결은 2개로 test했는데, 첫번째, rest api에서 받은 token을 붙여서 보내는것과, 두번째, 순상님이 준 token을 hard coding해서 보내봤다. [12:32 AM] 둘다 안된다. [12:32 AM] 낼 조금 더 봐야할 듯 하다.